diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a04179a --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Compiler files +cache/ +out/ + +# Dotenv file +.env + +broadcast/ + +node_modules/ + +.vscode/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 6580426..ac2b66a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/seaport-types"] path = lib/seaport-types - url = https://github.com/projectopensea/seaport-types + url = https://github.com/projectOpensea/seaport-types +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/foundry.toml b/foundry.toml index ddaf590..401707b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,7 +1,15 @@ [profile.default] +solc = "0.8.22" src = "src" out = "out" libs = ["lib"] -remappings = ['seaport-types/=lib/seaport-types/'] +remappings = [ + "seaport-types/=lib/seaport-types/", + "ds-test/=lib/forge-std/lib/ds-test/src/", + "forge-std/=lib/forge-std/src/" +] -# See more config options https://github.com/foundry-rs/foundry/tree/master/config +# solidity 0.8.22 defaults to the shanghai fork which introduces the PUSH0 opcode. +# Most chains outside of mainnet have not implemented this opcode yet, so we explicitly +# make sure solidity compiles using the paris fork to maintain cross-chain compatibility +evm_version = "paris" \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..2f11269 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 2f112697506eab12d433a65fdc31a639548fe365 diff --git a/lib/seaport-types b/lib/seaport-types index 0bd56c0..25bae8d 160000 --- a/lib/seaport-types +++ b/lib/seaport-types @@ -1 +1 @@ -Subproject commit 0bd56c08d485ee5b3d5a20b0482a35a8ada60256 +Subproject commit 25bae8ddfa8709e5c51ab429fe06024e46a18f15 diff --git a/src/lib/ConsiderationEncoder.sol b/src/lib/ConsiderationEncoder.sol index 146c7cd..8af5f48 100644 --- a/src/lib/ConsiderationEncoder.sol +++ b/src/lib/ConsiderationEncoder.sol @@ -57,7 +57,26 @@ import { ZoneParameters_zoneHash_offset } from "seaport-types/src/lib/ConsiderationConstants.sol"; -import {BasicOrderParameters, OrderParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import { + Rental_validateOrder_selector, + Rental_OrderParameters_orderType_offset, + Rental_ZoneParameters_orderHash_offset, + Rental_ZoneParameters_fulfiller_offset, + Rental_ZoneParameters_offerer_offset, + Rental_ZoneParameters_offer_head_offset, + Rental_ZoneParameters_consideration_head_offset, + Rental_ZoneParameters_totalExecutions_head_offset, + Rental_ZoneParameters_extraData_head_offset, + Rental_ZoneParameters_orderHashes_head_offset, + Rental_ZoneParameters_startTime_offset, + Rental_ZoneParameters_endTime_offset, + Rental_ZoneParameters_zoneHash_offset, + Rental_ZoneParameters_orderType_offset, + Rental_ZoneParameters_base_tail_offset +} from "./rental/ConsiderationConstants.sol"; +import {StructPointers} from "./rental/ConsiderationStructs.sol"; + +import {BasicOrderParameters, OrderParameters, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {CalldataPointer, getFreeMemoryPointer, MemoryPointer} from "seaport-types/src/helpers/PointerLibraries.sol"; @@ -312,6 +331,7 @@ contract ConsiderationEncoder { * updated/written to in this function. * * @param orderHash The order hash. + * @param totalExecutions The total transfers that occur during this order fulfillment * @param orderParameters The OrderParameters struct used to construct the * encoded `validateOrder` calldata. * @param extraData The extraData bytes array used to construct the @@ -326,6 +346,7 @@ contract ConsiderationEncoder { */ function _encodeValidateOrder( bytes32 orderHash, + ReceivedItem[] memory totalExecutions, OrderParameters memory orderParameters, bytes memory extraData, bytes32[] memory orderHashes @@ -335,7 +356,7 @@ contract ConsiderationEncoder { dst = getFreeMemoryPointer(); // Write validateOrder selector and get pointer to start of calldata. - dst.write(validateOrder_selector); + dst.write(Rental_validateOrder_selector); dst = dst.offset(validateOrder_selector_offset); // Get pointer to the beginning of the encoded data. @@ -349,74 +370,104 @@ contract ConsiderationEncoder { // Write orderHash and fulfiller to zoneParameters. dstHead.writeBytes32(orderHash); - dstHead.offset(ZoneParameters_fulfiller_offset).write(msg.sender); + dstHead.offset(Rental_ZoneParameters_fulfiller_offset).write(msg.sender); // Get the memory pointer to the order parameters struct. MemoryPointer src = orderParameters.toMemoryPointer(); - // Copy offerer, startTime, endTime and zoneHash to zoneParameters. - dstHead.offset(ZoneParameters_offerer_offset).write(src.readUint256()); - dstHead.offset(ZoneParameters_startTime_offset).write( + // Copy offerer, startTime, endTime, zoneHash, and orderType to zoneParameters. + dstHead.offset(Rental_ZoneParameters_offerer_offset).write(src.readUint256()); + dstHead.offset(Rental_ZoneParameters_startTime_offset).write( src.offset(OrderParameters_startTime_offset).readUint256() ); - dstHead.offset(ZoneParameters_endTime_offset).write(src.offset(OrderParameters_endTime_offset).readUint256()); - dstHead.offset(ZoneParameters_zoneHash_offset).write(src.offset(OrderParameters_zoneHash_offset).readUint256()); + dstHead.offset(Rental_ZoneParameters_endTime_offset).write(src.offset(OrderParameters_endTime_offset).readUint256()); + dstHead.offset(Rental_ZoneParameters_zoneHash_offset).write(src.offset(OrderParameters_zoneHash_offset).readUint256()); + dstHead.offset(Rental_ZoneParameters_orderType_offset).write(src.offset(Rental_OrderParameters_orderType_offset).readUint256()); // Initialize tail offset, used to populate the offer array. - uint256 tailOffset = ZoneParameters_base_tail_offset; + uint256 tailOffset = Rental_ZoneParameters_base_tail_offset; - // Write offset to `offer`. - dstHead.offset(ZoneParameters_offer_head_offset).write(tailOffset); + // Encode offer + { + // Write offset to `offer`. + dstHead.offset(Rental_ZoneParameters_offer_head_offset).write(tailOffset); - // Get pointer to `orderParameters.offer.length`. - MemoryPointer srcOfferPointer = src.offset(OrderParameters_offer_head_offset).readMemoryPointer(); + // Get pointer to `orderParameters.offer.length`. + MemoryPointer srcOfferPointer = src.offset(OrderParameters_offer_head_offset).readMemoryPointer(); - // Encode the offer array as a `SpentItem[]`. - uint256 offerSize = _encodeSpentItems(srcOfferPointer, dstHead.offset(tailOffset)); + // Encode the offer array as a `SpentItem[]`. + uint256 offerSize = _encodeSpentItems(srcOfferPointer, dstHead.offset(tailOffset)); - unchecked { - // Increment tail offset, now used to populate consideration array. - tailOffset += offerSize; + unchecked { + // Increment tail offset, now used to populate consideration array. + tailOffset += offerSize; + } } - // Write offset to consideration. - dstHead.offset(ZoneParameters_consideration_head_offset).write(tailOffset); + // Encode consideration + { + // Write offset to consideration. + dstHead.offset(Rental_ZoneParameters_consideration_head_offset).write(tailOffset); - // Get pointer to `orderParameters.consideration.length`. - MemoryPointer srcConsiderationPointer = - src.offset(OrderParameters_consideration_head_offset).readMemoryPointer(); + // Get pointer to `orderParameters.consideration.length`. + MemoryPointer srcConsiderationPointer = + src.offset(OrderParameters_consideration_head_offset).readMemoryPointer(); - // Encode the consideration array as a `ReceivedItem[]`. - uint256 considerationSize = - _encodeConsiderationAsReceivedItems(srcConsiderationPointer, dstHead.offset(tailOffset)); + // Encode the consideration array as a `ReceivedItem[]`. + uint256 considerationSize = + _encodeConsiderationAsReceivedItems(srcConsiderationPointer, dstHead.offset(tailOffset)); - unchecked { - // Increment tail offset, now used to populate extraData array. - tailOffset += considerationSize; + unchecked { + // Increment tail offset, now used to populate totalExecutions array. + tailOffset += considerationSize; + } } - // Write offset to extraData. - dstHead.offset(ZoneParameters_extraData_head_offset).write(tailOffset); - // Copy extraData. - uint256 extraDataSize = _encodeBytes(toMemoryPointer(extraData), dstHead.offset(tailOffset)); + // Encode totalExecutions + { + // Write offset to totalExecutions. + dstHead.offset(Rental_ZoneParameters_totalExecutions_head_offset).write(tailOffset); - unchecked { - // Increment tail offset, now used to populate orderHashes array. - tailOffset += extraDataSize; + // Encode the total exuections array. + uint256 totalExecutionsSize = _encodeConsiderationAsReceivedItems( + StructPointers.toMemoryPointer(totalExecutions), + dstHead.offset(tailOffset) + ); + + unchecked { + // Increment tail offset, now used to populate extraData array. + tailOffset += totalExecutionsSize; + } } - // Write offset to orderHashes. - dstHead.offset(ZoneParameters_orderHashes_head_offset).write(tailOffset); + // Encode extraData + { + // Write offset to extraData. + dstHead.offset(Rental_ZoneParameters_extraData_head_offset).write(tailOffset); + // Copy extraData. + uint256 extraDataSize = _encodeBytes(toMemoryPointer(extraData), dstHead.offset(tailOffset)); - // Encode the order hashes array. - uint256 orderHashesSize = _encodeOrderHashes(toMemoryPointer(orderHashes), dstHead.offset(tailOffset)); + unchecked { + // Increment tail offset, now used to populate orderHashes array. + tailOffset += extraDataSize; + } + } - unchecked { - // Increment the tail offset, now used to determine final size. - tailOffset += orderHashesSize; + // Encode orderHashes + { + // Write offset to orderHashes. + dstHead.offset(Rental_ZoneParameters_orderHashes_head_offset).write(tailOffset); + + // Encode the order hashes array. + uint256 orderHashesSize = _encodeOrderHashes(toMemoryPointer(orderHashes), dstHead.offset(tailOffset)); - // Derive final size including selector and ZoneParameters pointer. - size = ZoneParameters_selectorAndPointer_length + tailOffset; + unchecked { + // Increment the tail offset, now used to determine final size. + tailOffset += orderHashesSize; + + // Derive final size including selector and ZoneParameters pointer. + size = ZoneParameters_selectorAndPointer_length + tailOffset; + } } } @@ -652,4 +703,65 @@ contract ConsiderationEncoder { size = OneWord + (length * ReceivedItem_size); } } + + /** + * @dev Takes a memory pointer to order parameters and a memory + * pointer to a total executions array to copy it to, and copies + * the offer and consideration items as a ReceivedItem array. + * + * @param totalExecutions A memory pointer referencing the total executions + * that occur during this fulfillment. + * @param orderParameters A memory pointer referencing the location in memory to + * the order parameters, where the offer and consideration items + * will be copied from + */ + function _encodeTotalExecutions(ReceivedItem[] memory totalExecutions, OrderParameters memory orderParameters) internal view { + // Get the pointer to the length of the execution array + MemoryPointer executionLengthPtr = StructPointers.toMemoryPointer(totalExecutions); + + // Keep track of a memory pointer to the head of the execution array + MemoryPointer executionHeadPtr = executionLengthPtr.next(); + + // Keep track of a memory pointer to the tail of the execution array + MemoryPointer offerHeadEndPtr = executionLengthPtr.next().offset( + orderParameters.offer.length << OneWordShift + ); + + MemoryPointer considerationHeadEndPtr = offerHeadEndPtr.offset( + orderParameters.consideration.length << OneWordShift + ); + + // Get the memory pointer to the order parameters struct. + MemoryPointer opPtr = orderParameters.toMemoryPointer(); + + // Get pointer to `orderParameters.offer.length` plus one word to get to the start of the data + MemoryPointer opOfferPointer = opPtr.offset(OrderParameters_offer_head_offset).readMemoryPointer().next(); + + // Get pointer to `orderParameters.consideration.length` plus one word to get to the start of the data + MemoryPointer opConsiderationPointer = opPtr.offset(OrderParameters_consideration_head_offset).readMemoryPointer().next(); + + while (executionHeadPtr.lt(offerHeadEndPtr)) { + // load the memory location where the execution head pointer is + MemoryPointer offerTail = executionHeadPtr.pptr(); + + // copy the offer item into the execution array + opOfferPointer.pptr().copy(offerTail, ReceivedItem_size); + + // increment the head pointer and the offer pointer by one word + executionHeadPtr = executionHeadPtr.next(); + opOfferPointer = opOfferPointer.next(); + } + + while (executionHeadPtr.lt(considerationHeadEndPtr)) { + // load the memory location where the execution head pointer is + MemoryPointer considerationTail = executionHeadPtr.pptr(); + + // copy the consideration item into the execution array + opConsiderationPointer.pptr().copy(considerationTail, ReceivedItem_size); + + // increment the head pointer and the consideration pointer by one word + executionHeadPtr = executionHeadPtr.next(); + opConsiderationPointer = opConsiderationPointer.next(); + } + } } diff --git a/src/lib/OrderCombiner.sol b/src/lib/OrderCombiner.sol index d00109d..70f4729 100644 --- a/src/lib/OrderCombiner.sol +++ b/src/lib/OrderCombiner.sol @@ -617,6 +617,40 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { return (availableOrders, executions); } + /** + * @dev Internal function to discover the amount of offer items that have + * currently not been spent. Used to help determine the size of the + * total executions array + * + * @param advancedOrders The orders to check executions for. + * + * @return totalItems Total number of offer items in the advanced order + * that have not yet been spent. + */ + function _discoverUnspentOfferItems( + AdvancedOrder[] memory advancedOrders + ) internal pure returns (uint256 totalItems) { + // Iterate over advanced orders + for (uint256 i = 0; i < advancedOrders.length; ++i) { + // Retrieve the order + AdvancedOrder memory order = advancedOrders[i]; + + // Read length of offer item array and place on the stack. + uint256 totalOfferItems = order.parameters.offer.length; + + // Iterate over each offer item + for (uint256 j = 0; j < totalOfferItems; ++j) { + // get the offer item + OfferItem memory offerItem = order.parameters.offer[j]; + + // if the unspent amount is not zero, then mark the offer item + if (offerItem.startAmount != 0) { + ++totalItems; + } + } + } + } + /** * @dev Internal function to perform a final check that each consideration * item for an arbitrary number of fulfilled orders has been met and to @@ -651,6 +685,13 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { // Initialize array for tracking available orders. bool[] memory availableOrders = new bool[](totalOrders); + // discover the number of unspent offer items, since these are not marked as `Execution` structs by seaport. + uint256 unspentItemsLength = _discoverUnspentOfferItems(advancedOrders); + + // an array which will hold all the execution items made by seaport, including any unspent offer items + // which are transferred to the recipient address + ReceivedItem[] memory totalExecutionItems = new ReceivedItem[](executions.length + unspentItemsLength); + // Initialize an accumulator array. From this point forward, no new // memory regions can be safely allocated until the accumulator is no // longer being utilized, as the accumulator operates in an open-ended @@ -684,6 +725,9 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { } } + // add the received item to the total received items array + totalExecutionItems[i] = item; + // Transfer the item specified by the execution. _transfer(item, execution.offerer, execution.conduitKey, accumulator); @@ -694,8 +738,16 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { } } + // place recipient back on the stack to avoid stack too deep + address _recipient = recipient; + // Skip overflow checks as all for loops are indexed starting at zero. unchecked { + + // a counter for all the unspent items that have been processed and added to the `totalExecutionItems` array. + // It is initialized to `executions.length` because each execution will be added to the received items array first. + uint256 totalProcessedExecutionItems = executions.length; + // Iterate over each order. for (uint256 i = 0; i < totalOrders; ++i) { // Retrieve the order in question. @@ -731,11 +783,22 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { // Note that the transfer will not be reflected in the // executions array. if (offerItem.startAmount != 0) { + // add the received item to the array of total executions. Use this method to + // avoid creating any new memory regions + totalExecutionItems[totalProcessedExecutionItems].itemType = offerItem.itemType; + totalExecutionItems[totalProcessedExecutionItems].token = offerItem.token; + totalExecutionItems[totalProcessedExecutionItems].identifier = offerItem.identifierOrCriteria; + totalExecutionItems[totalProcessedExecutionItems].amount = offerItem.startAmount; + totalExecutionItems[totalProcessedExecutionItems].recipient = payable(_recipient); + + // increment the number of processed execution items + totalProcessedExecutionItems++; + // Replace the endAmount parameter with the recipient to // make offerItem compatible with the ReceivedItem input // to _transfer and cache the original endAmount so it // can be restored after the transfer. - uint256 originalEndAmount = _replaceEndAmountWithRecipient(offerItem, recipient); + uint256 originalEndAmount = _replaceEndAmountWithRecipient(offerItem, _recipient); // Transfer excess offer item amount to recipient. _toOfferItemInput(_transfer)( @@ -808,7 +871,7 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { // Ensure the order in question is being fulfilled. if (availableOrders[i]) { // Check restricted orders and contract orders. - _assertRestrictedAdvancedOrderValidity(advancedOrders[i], orderHashes, orderHashes[i]); + _assertRestrictedAdvancedOrderValidity(advancedOrders[i], totalExecutionItems, orderHashes, orderHashes[i]); } // Skip overflow checks as for loop is indexed starting at zero. diff --git a/src/lib/OrderFulfiller.sol b/src/lib/OrderFulfiller.sol index 6006a70..214072c 100644 --- a/src/lib/OrderFulfiller.sol +++ b/src/lib/OrderFulfiller.sol @@ -104,14 +104,20 @@ contract OrderFulfiller is BasicOrderFulfiller, CriteriaResolution, AmountDerive OrderParameters memory orderParameters = advancedOrders[0].parameters; // Perform each item transfer with the appropriate fractional amount. - _applyFractionsAndTransferEach(orderParameters, fillNumerator, fillDenominator, fulfillerConduitKey, recipient); + ReceivedItem[] memory totalExecutions = _applyFractionsAndTransferEach( + orderParameters, + fillNumerator, + fillDenominator, + fulfillerConduitKey, + recipient + ); // Declare empty bytes32 array and populate with the order hash. bytes32[] memory orderHashes = new bytes32[](1); orderHashes[0] = orderHash; // Ensure restricted orders have a valid submitter or pass a zone check. - _assertRestrictedAdvancedOrderValidity(advancedOrders[0], orderHashes, orderHash); + _assertRestrictedAdvancedOrderValidity(advancedOrders[0], totalExecutions, orderHashes, orderHash); // Emit an event signifying that the order has been fulfilled. _emitOrderFulfilledEvent( @@ -151,7 +157,12 @@ contract OrderFulfiller is BasicOrderFulfiller, CriteriaResolution, AmountDerive uint256 denominator, bytes32 fulfillerConduitKey, address recipient - ) internal { + ) internal returns (ReceivedItem[] memory totalExecutions) { + // Initialize total executions as the length of offer and consideration items + totalExecutions = new ReceivedItem[]( + orderParameters.offer.length + orderParameters.consideration.length + ); + // Read start time & end time from order parameters and place on stack. uint256 startTime = orderParameters.startTime; uint256 endTime = orderParameters.endTime; @@ -332,6 +343,10 @@ contract OrderFulfiller is BasicOrderFulfiller, CriteriaResolution, AmountDerive if (nativeTokenBalance != 0) { _transferNativeTokens(payable(msg.sender), nativeTokenBalance); } + + // encode the offer items and consideration items into a single array of executions. This step is done last + // because the offer and consideration items must first be converted to received items. + _encodeTotalExecutions(totalExecutions, orderParameters); } /** diff --git a/src/lib/ZoneInteraction.sol b/src/lib/ZoneInteraction.sol index 53387e1..89618b1 100644 --- a/src/lib/ZoneInteraction.sol +++ b/src/lib/ZoneInteraction.sol @@ -3,7 +3,12 @@ pragma solidity ^0.8.17; import {OrderType} from "seaport-types/src/lib/ConsiderationEnums.sol"; -import {AdvancedOrder, BasicOrderParameters, OrderParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import { + AdvancedOrder, + BasicOrderParameters, + OrderParameters, + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {ZoneInteractionErrors} from "seaport-types/src/interfaces/ZoneInteractionErrors.sol"; @@ -69,13 +74,15 @@ contract ZoneInteraction is ConsiderationEncoder, ZoneInteractionErrors, LowLeve * correct magic value returned. Contract orders must successfully call * `ratifyOrder` with the correct magic value returned. * - * @param advancedOrder The advanced order in question. - * @param orderHashes The order hashes of each order included as part of - * the current fulfillment. - * @param orderHash The hash of the order. + * @param advancedOrder The advanced order in question. + * @param totalExecutions The total transfers in the current fulfillment + * @param orderHashes The order hashes of each order included as part of + * the current fulfillment. + * @param orderHash The hash of the order. */ function _assertRestrictedAdvancedOrderValidity( AdvancedOrder memory advancedOrder, + ReceivedItem[] memory totalExecutions, bytes32[] memory orderHashes, bytes32 orderHash ) internal { @@ -91,7 +98,7 @@ contract ZoneInteraction is ConsiderationEncoder, ZoneInteractionErrors, LowLeve // OrderType 2-3 require zone to be caller or approve via validateOrder. if (_isRestrictedAndCallerNotZone(parameters.orderType, parameters.zone)) { // Encode the `validateOrder` call in memory. - (callData, size) = _encodeValidateOrder(orderHash, parameters, advancedOrder.extraData, orderHashes); + (callData, size) = _encodeValidateOrder(orderHash, totalExecutions, parameters, advancedOrder.extraData, orderHashes); // Set the target to the zone. target = (parameters.toMemoryPointer().offset(OrderParameters_zone_offset).readAddress()); diff --git a/src/lib/rental/ConsiderationConstants.sol b/src/lib/rental/ConsiderationConstants.sol new file mode 100644 index 0000000..8819bed --- /dev/null +++ b/src/lib/rental/ConsiderationConstants.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +uint256 constant Rental_validateOrder_selector = 0x0064e1d4; + +// Zone Parameters +uint256 constant Rental_ZoneParameters_orderHash_offset = 0x00; +uint256 constant Rental_ZoneParameters_fulfiller_offset = 0x20; +uint256 constant Rental_ZoneParameters_offerer_offset = 0x40; +uint256 constant Rental_ZoneParameters_offer_head_offset = 0x60; +uint256 constant Rental_ZoneParameters_consideration_head_offset = 0x80; +uint256 constant Rental_ZoneParameters_totalExecutions_head_offset = 0xa0; +uint256 constant Rental_ZoneParameters_extraData_head_offset = 0xc0; +uint256 constant Rental_ZoneParameters_orderHashes_head_offset = 0xe0; +uint256 constant Rental_ZoneParameters_startTime_offset = 0x100; +uint256 constant Rental_ZoneParameters_endTime_offset = 0x120; +uint256 constant Rental_ZoneParameters_zoneHash_offset = 0x140; +uint256 constant Rental_ZoneParameters_orderType_offset = 0x160; +uint256 constant Rental_ZoneParameters_base_tail_offset = 0x180; + +// Order Parameters +uint256 constant Rental_OrderParameters_orderType_offset = 0x80; diff --git a/src/lib/rental/ConsiderationStructs.sol b/src/lib/rental/ConsiderationStructs.sol new file mode 100644 index 0000000..05d23c7 --- /dev/null +++ b/src/lib/rental/ConsiderationStructs.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { OrderType } from "seaport-types/src/lib/ConsiderationEnums.sol"; + +import { + ReceivedItem, + SpentItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {MemoryPointer} from "seaport-types/src/helpers/PointerLibraries.sol"; + +import { ConduitTransfer } from "seaport-types/src/conduit/lib/ConduitStructs.sol"; + + +/** + * @dev Restricted orders are validated post-execution by calling validateOrder + * on the zone. This struct provides context about the order fulfillment + * and any supplied extraData, as well as all order hashes fulfilled in a + * call to a match or fulfillAvailable method. + */ +struct ZoneParameters { + bytes32 orderHash; + address fulfiller; + address offerer; + SpentItem[] offer; + ReceivedItem[] consideration; + ReceivedItem[] totalExecutions; + bytes extraData; + bytes32[] orderHashes; + uint256 startTime; + uint256 endTime; + bytes32 zoneHash; + OrderType orderType; +} + +library StructPointers { + function toMemoryPointer( + ReceivedItem[] memory obj + ) internal pure returns (MemoryPointer ptr) { + assembly { + ptr := obj + } + } +} diff --git a/test/ConsiderationEncoder.sol b/test/ConsiderationEncoder.sol new file mode 100644 index 0000000..16b675a --- /dev/null +++ b/test/ConsiderationEncoder.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; +import {Test} from "forge-std/Test.sol"; +import { + AdvancedOrder, + OrderParameters, + ZoneParameters, + SpentItem, + ReceivedItem, + ConsiderationItem, + OfferItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {OrderType, ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {CalldataPointer, getFreeMemoryPointer, MemoryPointer} from "seaport-types/src/helpers/PointerLibraries.sol"; + +import {ConsiderationEncoder} from "src/lib/ConsiderationEncoder.sol"; +import {StructPointers} from "src/lib/rental/ConsiderationStructs.sol"; + +import {Utils} from "test/helpers/Utils.sol"; + +contract ConsiderationEncoder_Test is Test, ConsiderationEncoder { + + function test_Success_EncodeValidateOrder() public { + // create offer + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC20, + token: Utils.mockAddress("ERC20"), + identifierOrCriteria: 9, + startAmount: 150, + endAmount: 150 + }); + + // create consideration + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: Utils.mockAddress("ERC721"), + identifierOrCriteria: 5, + startAmount: 1, + endAmount: 1, + recipient: payable(Utils.mockAddress("recipient")) + }); + + // convert the consideration items into received items silently + Utils.convertConsiderationIntoReceivedItem(consideration); + + // create total executions + ReceivedItem[] memory totalExecutions = new ReceivedItem[](2); + totalExecutions[0] = ReceivedItem({ + itemType: ItemType.ERC721, + token: Utils.mockAddress("ERC721"), + identifier: 5, + amount: 1, + recipient: payable(Utils.mockAddress("recipient")) + }); + totalExecutions[1] = ReceivedItem({ + itemType: ItemType.ERC20, + token: Utils.mockAddress("ERC20"), + identifier: 9, + amount: 150, + recipient: payable(Utils.mockAddress("recipient")) + }); + + // create the order parameters + OrderParameters memory orderParameters = OrderParameters({ + offerer: Utils.mockAddress("offerer"), + zone: Utils.mockAddress("zone"), + offer: offer, + consideration: consideration, + orderType: OrderType.FULL_RESTRICTED, + startTime: block.timestamp, + endTime: block.timestamp + 5, + zoneHash: keccak256("zone hash"), + salt: 123456789, + conduitKey: keccak256("conduit key"), + totalOriginalConsiderationItems: 1 + }); + + // create the order hash + bytes32 orderHash = keccak256("order hash"); + + // create the order hashes + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = orderHash; + + _encodeValidateOrder( + orderHash, + totalExecutions, + orderParameters, + bytes("extra data"), + orderHashes + ); + + assertTrue(true); + } +} \ No newline at end of file diff --git a/test/OrderCombiner.sol b/test/OrderCombiner.sol new file mode 100644 index 0000000..d016e68 --- /dev/null +++ b/test/OrderCombiner.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; +import { + AdvancedOrder, + OrderParameters, + ZoneParameters, + SpentItem, + ReceivedItem, + ConsiderationItem, + OfferItem, + Execution +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {OrderType, ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {CalldataPointer, getFreeMemoryPointer, MemoryPointer} from "seaport-types/src/helpers/PointerLibraries.sol"; + +import {OrderCombinerHarness} from "test/harnesses/OrderCombinerHarness.sol"; +import {StructPointers} from "src/lib/rental/ConsiderationStructs.sol"; +import {ConduitController} from "src/conduit/ConduitController.sol"; + +import {Utils} from "test/helpers/Utils.sol"; +import {Assertions} from "test/helpers/Assertions.sol"; +import {Zone} from "test/helpers/Zone.sol"; + +import "forge-std/console.sol"; + +// Seaport performs checks to make sure tokens are contracts +contract MockToken {} + +contract OrderCombiner_Test is Assertions { + address mockConduitController; + address mockERC20; + address mockERC721; + address mockOfferer; + address mockRecipient; + + OrderCombinerHarness orderCombiner; + Zone public zone; + + function setUp() public { + // set mock addresses + mockOfferer = Utils.mockAddress("offerer"); + mockRecipient = Utils.mockAddress("recipient"); + mockConduitController = Utils.mockAddress("conduitController"); + mockERC20 = address(new MockToken()); + mockERC721 = address(new MockToken()); + + // mock a setup call to the conduit controller + vm.mockCall( + mockConduitController, + abi.encodeWithSelector(ConduitController.getConduitCodeHashes.selector), + abi.encode(bytes32(0), bytes32(0)) + ); + + orderCombiner = new OrderCombinerHarness(mockConduitController); + zone = new Zone(); + + vm.label(address(orderCombiner), "orderCombiner"); + vm.label(mockERC20, "mockERC20"); + vm.label(mockERC721, "mockERC721"); + } + + function test_Success_PerformFinalChecksAndExecuteOrders() public { + // create offer + OfferItem[] memory offer = new OfferItem[](2); + offer[0] = OfferItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifierOrCriteria: 0, + startAmount: 0, + endAmount: 150 + }); + offer[1] = OfferItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifierOrCriteria: 0, + startAmount: 0, + endAmount: 160 + }); + + // create another offer which will not have a matching consideration + OfferItem[] memory unmatchedOffer = new OfferItem[](1); + unmatchedOffer[0] = OfferItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifierOrCriteria: 0, + startAmount: 200, + endAmount: 200 + }); + + // create consideration + ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: mockERC721, + identifierOrCriteria: 5, + startAmount: 0, + endAmount: uint256(1), + recipient: payable(mockOfferer) + }); + consideration[1] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: mockERC721, + identifierOrCriteria: 6, + startAmount: 0, + endAmount: uint256(1), + recipient: payable(mockOfferer) + }); + + // create the order parameters + OrderParameters memory orderParameters = OrderParameters({ + offerer: mockOfferer, + zone: address(zone), + offer: offer, + consideration: consideration, + orderType: OrderType.FULL_RESTRICTED, + startTime: block.timestamp, + endTime: block.timestamp + 5, + zoneHash: keccak256("zone hash"), + salt: 123456789, + conduitKey: bytes32(0), + totalOriginalConsiderationItems: 1 + }); + + // create a second order parameter struct + OrderParameters memory secondOrderParameters = OrderParameters({ + offerer: mockOfferer, + zone: address(zone), + offer: unmatchedOffer, + consideration: new ConsiderationItem[](0), + orderType: OrderType.FULL_RESTRICTED, + startTime: block.timestamp, + endTime: block.timestamp + 5, + zoneHash: keccak256("zone hash"), + salt: 123456789, + conduitKey: bytes32(0), + totalOriginalConsiderationItems: 0 + }); + + // create the advanced order array + AdvancedOrder[] memory advancedOrders = new AdvancedOrder[](2); + advancedOrders[0] = AdvancedOrder({ + parameters: orderParameters, + numerator: 1, + denominator: 1, + signature: "signature", + extraData: "extraData" + }); + advancedOrders[1] = AdvancedOrder({ + parameters: secondOrderParameters, + numerator: 1, + denominator: 1, + signature: "signature", + extraData: "extraData" + }); + + // create the executions array + Execution[] memory executions = new Execution[](offer.length + consideration.length); + for (uint256 i = 0; i < executions.length; ++i) { + if (i < offer.length) { + executions[i] = Utils.offerToExecution( + offer[i], + mockOfferer, + mockRecipient, + bytes32(0) + ); + } else { + executions[i] = Utils.considerationToExecution( + consideration[i - offer.length], + mockOfferer, + bytes32(0) + ); + } + } + + // create the order hashes + bytes32[] memory orderHashes = new bytes32[](2); + orderHashes[0] = keccak256("order hash"); + orderHashes[1] = keccak256("order hash 2"); + + // mock a transferFrom() call for the ERC20 + vm.mockCall( + mockERC20, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + vm.mockCall( + mockERC20, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + + // mock a transferFrom() call for the ERC721 + vm.mockCall( + mockERC721, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + vm.mockCall( + mockERC721, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + vm.mockCall( + mockERC721, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + + // call the order combiner. Mark this contract as the recipient + // of any unspent offer items + orderCombiner.performFinalChecksAndExecuteOrders( + advancedOrders, + executions, + orderHashes, + address(this), + true + ); + + ReceivedItem[] memory outputtedExecutions = zone.totalExecutions(); + + assertEq(outputtedExecutions.length, 10); + } +} \ No newline at end of file diff --git a/test/OrderFulfiller.sol b/test/OrderFulfiller.sol new file mode 100644 index 0000000..4f724f6 --- /dev/null +++ b/test/OrderFulfiller.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; +import { + AdvancedOrder, + OrderParameters, + ZoneParameters, + SpentItem, + ReceivedItem, + ConsiderationItem, + OfferItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {OrderType, ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {CalldataPointer, getFreeMemoryPointer, MemoryPointer} from "seaport-types/src/helpers/PointerLibraries.sol"; + +import {OrderFulfillerHarness} from "test/harnesses/OrderFulfillerHarness.sol"; +import {StructPointers} from "src/lib/rental/ConsiderationStructs.sol"; +import {ConduitController} from "src/conduit/ConduitController.sol"; + +import {Utils} from "test/helpers/Utils.sol"; +import {Assertions} from "test/helpers/Assertions.sol"; + +// Seaport performs checks to make sure tokens are contracts +contract MockToken {} + +contract OrderFulfiller_Test is Assertions { + address mockConduitController; + address mockERC20; + address mockERC721; + + OrderFulfillerHarness orderFulfiller; + + function setUp() public { + // set mock addresses + mockConduitController = Utils.mockAddress("conduitController"); + mockERC20 = address(new MockToken()); + mockERC721 = address(new MockToken()); + + // mock a setup call to the conduit controller + vm.mockCall( + mockConduitController, + abi.encodeWithSelector(ConduitController.getConduitCodeHashes.selector), + abi.encode(bytes32(0), bytes32(0)) + ); + + orderFulfiller = new OrderFulfillerHarness(mockConduitController); + + vm.label(address(orderFulfiller), "orderFulfiller"); + vm.label(mockERC20, "mockERC20"); + vm.label(mockERC721, "mockERC721"); + } + + function test_Success_ApplyFractionsAndTransferEach() public { + // create offer + OfferItem[] memory offer = new OfferItem[](2); + offer[0] = OfferItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifierOrCriteria: 0, + startAmount: 150, + endAmount: 150 + }); + offer[1] = OfferItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifierOrCriteria: 0, + startAmount: 160, + endAmount: 160 + }); + + // create consideration + ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: mockERC721, + identifierOrCriteria: 5, + startAmount: 1, + endAmount: 1, + recipient: payable(address(this)) + }); + consideration[1] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: mockERC721, + identifierOrCriteria: 6, + startAmount: 1, + endAmount: 1, + recipient: payable(address(this)) + }); + + // convert the consideration items into received items silently + Utils.convertConsiderationIntoReceivedItem(consideration); + + // create expected executions + ReceivedItem[] memory expectedExecutions = new ReceivedItem[](4); + for (uint256 i = 0; i < expectedExecutions.length; ++i) { + if (i < offer.length) { + OfferItem memory offerItem = offer[i]; + expectedExecutions[i] = ReceivedItem({ + itemType: offerItem.itemType, + token: offerItem.token, + identifier: offerItem.identifierOrCriteria, + amount: offerItem.startAmount, + recipient: payable(address(this)) + }); + } else { + ConsiderationItem memory considerationItem = consideration[i - offer.length]; + expectedExecutions[i] = ReceivedItem({ + itemType: considerationItem.itemType, + token: considerationItem.token, + identifier: considerationItem.identifierOrCriteria, + amount: considerationItem.startAmount, + recipient: payable(address(this)) + }); + } + } + + // create the order parameters + OrderParameters memory orderParameters = OrderParameters({ + offerer: Utils.mockAddress("offerer"), + zone: Utils.mockAddress("zone"), + offer: offer, + consideration: consideration, + orderType: OrderType.FULL_RESTRICTED, + startTime: block.timestamp, + endTime: block.timestamp + 5, + zoneHash: keccak256("zone hash"), + salt: 123456789, + conduitKey: bytes32(0), + totalOriginalConsiderationItems: 1 + }); + + // mock a transferFrom() call for the ERC20 + vm.mockCall( + mockERC20, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + + // mock a transferFrom() call for the ERC721 + vm.mockCall( + mockERC721, + abi.encodeWithSelector(bytes4(keccak256("transferFrom(address,address,uint256)"))), + abi.encode(true) + ); + + // get the outputted executions + ReceivedItem[] memory executions = orderFulfiller.applyFractionsAndTransferEach( + orderParameters, + 1, + 1, + bytes32(0), // dont use conduit key to perform transfer directly + address(this) + ); + + assertEq(expectedExecutions, executions); + } +} \ No newline at end of file diff --git a/test/ZoneInteraction.sol b/test/ZoneInteraction.sol new file mode 100644 index 0000000..b57f2c3 --- /dev/null +++ b/test/ZoneInteraction.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; +import {Test} from "forge-std/Test.sol"; +import { + AdvancedOrder, + OrderParameters, + SpentItem, + ReceivedItem, + ConsiderationItem, + OfferItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {OrderType, ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; + +import {ZoneInteraction} from "src/lib/ZoneInteraction.sol"; +import {ZoneParameters} from "src/lib/rental/ConsiderationStructs.sol"; + +import {Zone} from "test/helpers/Zone.sol"; +import {Utils} from "test/helpers/Utils.sol"; +import "forge-std/console.sol"; +contract ZoneInteraction_Test is Test, ZoneInteraction { + address mockERC20; + address mockERC721; + address mockOfferer; + address mockRecipient; + + Zone public zone; + Vm.Wallet public wallet; + + function setUp() public { + // set mock addresses + mockERC20 = Utils.mockAddress("mockERC20"); + mockERC721 = Utils.mockAddress("mockERC721"); + mockOfferer = Utils.mockAddress("offerer"); + mockRecipient = Utils.mockAddress("recipient"); + + zone = new Zone(); + wallet = vm.createWallet("wallet"); + } + + function test_Success_AssertRestrictedAdvancedOrderValidity() public { + + // create offer + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifierOrCriteria: 9, + startAmount: 150, + endAmount: 150 + }); + + // create consideration + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: mockERC721, + identifierOrCriteria: 5, + startAmount: 1, + endAmount: 1, + recipient: payable(mockRecipient) + }); + + // convert the consideration items into received items silently + Utils.convertConsiderationIntoReceivedItem(consideration); + + // create total executions + ReceivedItem[] memory totalExecutions = new ReceivedItem[](2); + totalExecutions[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: mockERC20, + identifier: 9, + amount: 150, + recipient: payable(mockRecipient) + }); + totalExecutions[1] = ReceivedItem({ + itemType: ItemType.ERC721, + token: mockERC721, + identifier: 5, + amount: 1, + recipient: payable(mockOfferer) + }); + + // create the order parameters + OrderParameters memory orderParameters = OrderParameters({ + offerer: wallet.addr, + zone: address(zone), + offer: offer, + consideration: consideration, + orderType: OrderType.FULL_RESTRICTED, + startTime: block.timestamp, + endTime: block.timestamp + 5, + zoneHash: keccak256("zone hash"), + salt: 123456789, + conduitKey: keccak256("conduit key"), + totalOriginalConsiderationItems: 1 + }); + + // create the advanced order + AdvancedOrder memory advancedOrder = AdvancedOrder({ + parameters: orderParameters, + numerator: 1, + denominator: 1, + signature: "signature", + extraData: "extraData" + }); + + // create the order hash + bytes32 orderHash = keccak256("order hash"); + + // create the order hashes + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = orderHash; + + // make a call to the zone + _assertRestrictedAdvancedOrderValidity( + advancedOrder, + totalExecutions, + orderHashes, + orderHash + ); + + assertEq(orderHash, zone.orderHash()); + } +} \ No newline at end of file diff --git a/test/harnesses/OrderCombinerHarness.sol b/test/harnesses/OrderCombinerHarness.sol new file mode 100644 index 0000000..a296747 --- /dev/null +++ b/test/harnesses/OrderCombinerHarness.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { + AdvancedOrder, + Execution +} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import {OrderCombiner} from "src/lib/OrderCombiner.sol"; + +contract OrderCombinerHarness is OrderCombiner { + + constructor(address conduitController) OrderCombiner(conduitController) {} + + function performFinalChecksAndExecuteOrders( + AdvancedOrder[] memory advancedOrders, + Execution[] memory executions, + bytes32[] memory orderHashes, + address recipient, + bool containsNonOpen + ) external returns (bool[] memory availableOrders) { + availableOrders = _performFinalChecksAndExecuteOrders( + advancedOrders, + executions, + orderHashes, + recipient, + containsNonOpen + ); + } +} \ No newline at end of file diff --git a/test/harnesses/OrderFulfillerHarness.sol b/test/harnesses/OrderFulfillerHarness.sol new file mode 100644 index 0000000..e75c9c1 --- /dev/null +++ b/test/harnesses/OrderFulfillerHarness.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { + OrderParameters, + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import {OrderFulfiller} from "src/lib/OrderFulfiller.sol"; + +contract OrderFulfillerHarness is OrderFulfiller { + + constructor(address conduitController) OrderFulfiller(conduitController) {} + + function applyFractionsAndTransferEach( + OrderParameters memory orderParameters, + uint256 numerator, + uint256 denominator, + bytes32 fulfillerConduitKey, + address recipient + ) external returns (ReceivedItem[] memory totalExecutions) { + totalExecutions = _applyFractionsAndTransferEach(orderParameters, numerator, denominator, fulfillerConduitKey, recipient); + } +} \ No newline at end of file diff --git a/test/helpers/Assertions.sol b/test/helpers/Assertions.sol new file mode 100644 index 0000000..34de8d3 --- /dev/null +++ b/test/helpers/Assertions.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; + +import { + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +abstract contract Assertions is Test { + // Compares two `ItemType` enum entities + function assertEq(ItemType a, ItemType b) internal { + assertEq(uint256(a), uint256(b), "itemType"); + } + + // Compares two `ReceivedItem` struct entities + function assertEq(ReceivedItem memory a, ReceivedItem memory b) internal { + assertEq(a.itemType, b.itemType); + assertEq(a.token, b.token, "token"); + assertEq(a.amount, b.amount, "amount"); + assertEq(a.identifier, b.identifier, "identifier"); + assertEq(a.recipient, b.recipient, "recipient"); + } + + // Compares two `ReceivedItem[]` struct entities + function assertEq(ReceivedItem[] memory a, ReceivedItem[] memory b) internal { + assertEq(a.length, b.length, "length mismatch"); + + for (uint256 i = 0; i < a.length; ++i) { + assertEq(a[0], b[0]); + } + } +} \ No newline at end of file diff --git a/test/helpers/Utils.sol b/test/helpers/Utils.sol new file mode 100644 index 0000000..c38c7fe --- /dev/null +++ b/test/helpers/Utils.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; + +import {getFreeMemoryPointer, MemoryPointer} from "seaport-types/src/helpers/PointerLibraries.sol"; +import { + ReceivedItem, + ConsiderationItem, + OfferItem, + Execution +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import { + ReceivedItem_amount_offset, + ReceivedItem_recipient_offset, + ConsiderationItem_recipient_offset +} from "seaport-types/src/lib/ConsiderationConstants.sol"; + +library Utils { + address private constant VM_ADDRESS = + address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + Vm private constant vm = Vm(VM_ADDRESS); + + // converts a bytes object into a memory pointer + function toMemoryPointer(bytes memory obj) internal pure returns (MemoryPointer ptr) { + assembly { + ptr := obj + } + } + + // creates a mock address from a string + function mockAddress(string memory addressString) internal pure returns (address) { + return address(uint160(uint256(keccak256(abi.encode(addressString))))); + } + + // In `_applyFractionsAndTransferEach`, the consideration items are silently + // converted into received items. This mimics that transformation. + function convertConsiderationIntoReceivedItem(ConsiderationItem[] memory consideration) internal pure { + for (uint256 i = 0; i < consideration.length; ++i) { + // Retrieve the consideration item. + ConsiderationItem memory considerationItem = consideration[i]; + + // fetch the amount + uint256 amount = considerationItem.startAmount; + + // Use assembly to set overloaded considerationItem arguments. + assembly { + // Write derived fractional amount to startAmount as amount. + mstore(add(considerationItem, ReceivedItem_amount_offset), amount) + + // Write original recipient to endAmount as recipient. + mstore( + add(considerationItem, ReceivedItem_recipient_offset), + mload(add(considerationItem, ConsiderationItem_recipient_offset)) + ) + } + } + } + + // Fetches the memory from a memory pointer with a specified size + function fetchMemory(MemoryPointer currentPointer, uint256 size) internal pure returns (bytes memory data) { + // fetch a pointer to the end of the memory + MemoryPointer end = currentPointer.offset(size); + + // create a placeholder variable for the data + MemoryPointer dataLengthPtr = toMemoryPointer(data); + MemoryPointer currentData = toMemoryPointer(data).offset(0x20); + + // loop through the memory, one word at a time + while (currentPointer.lt(end)) { + // get the length + uint256 length = dataLengthPtr.readUint256(); + + // increase the length + dataLengthPtr.writeBytes32(bytes32(length + 32)); + + // write the value to the output data bytes value + currentData.writeBytes32(currentPointer.readBytes32()); + + // increment pointers + currentData = currentData.next(); + currentPointer = currentPointer.next(); + } + + return data; + } + + function offerToExecution( + OfferItem memory offer, + address offerer, + address recipient, + bytes32 conduitKey + ) internal pure returns (Execution memory execution) { + execution = Execution({ + item: ReceivedItem({ + itemType: offer.itemType, + token: offer.token, + identifier: offer.identifierOrCriteria, + amount: offer.endAmount, + recipient: payable(recipient) + }), + offerer: offerer, + conduitKey: conduitKey + }); + } + + function considerationToExecution( + ConsiderationItem memory consideration, + address offerer, + bytes32 conduitKey + ) internal pure returns (Execution memory execution) { + execution = Execution({ + item: ReceivedItem({ + itemType: consideration.itemType, + token: consideration.token, + identifier: consideration.identifierOrCriteria, + amount: consideration.endAmount, + recipient: consideration.recipient + }), + offerer: offerer, + conduitKey: conduitKey + }); + } + +} \ No newline at end of file diff --git a/test/helpers/Zone.sol b/test/helpers/Zone.sol new file mode 100644 index 0000000..19f843f --- /dev/null +++ b/test/helpers/Zone.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {SpentItem, ReceivedItem, OrderType} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import {ZoneParameters} from "src/lib/rental/ConsiderationStructs.sol"; + +contract Zone { + + // public valuues to test against + bytes32 public orderHash; + address public fulfiller; + address public offerer; + SpentItem[] public offer; + ReceivedItem[] public consideration; + ReceivedItem[] private _totalExecutions; + bytes public extraData; + bytes32[] public orderHashes; + uint256 public startTime; + uint256 public endTime; + bytes32 public zoneHash; + OrderType public orderType; + + function validateOrder(ZoneParameters calldata zoneParams) external returns (bytes4 validOrderMagicValue) { + + // store all zone parameters + orderHash = zoneParams.orderHash; + fulfiller = zoneParams.fulfiller; + offerer = zoneParams.offerer; + extraData = zoneParams.extraData; + orderHashes = zoneParams.orderHashes; + startTime = zoneParams.startTime; + endTime = zoneParams.endTime; + zoneHash = zoneParams.zoneHash; + orderType = zoneParams.orderType; + + // push all offer items + for (uint256 i; i < zoneParams.offer.length; ++i) { + offer.push(zoneParams.offer[i]); + } + + // push all consideration items + for (uint256 i = 0; i < zoneParams.consideration.length; ++i) { + consideration.push(zoneParams.consideration[i]); + } + + // push all execution items + for (uint256 i = 0; i < zoneParams.totalExecutions.length; ++i) { + _totalExecutions.push(zoneParams.totalExecutions[i]); + } + + // return the selector + validOrderMagicValue = Zone.validateOrder.selector; + } + + function totalExecutions() external view returns (ReceivedItem[] memory items) { + return _totalExecutions; + } +} \ No newline at end of file