diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index d14f2241..5ffdef8e 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -53,6 +53,15 @@ jobs: run: | yarn install --frozen-lockfile --network-concurrency 1 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge Version + run: forge --version + + - name: Install Forge dependancies + run: forge install + - name: Compile contracts run: | yarn compile @@ -61,7 +70,7 @@ jobs: run: | rm -rf dist && yarn build - # ! Do NOT remove - this will cause a Sev 0 incident ! + # ! Do NOT remove - this will cause a Sev 0 incident ! - name: Pack NPM package run: | npm pack diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db7fba87..441fa4e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,12 @@ jobs: cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile --network-concurrency 1 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Show Forge Version + run: forge --version + - name: Install Forge dependancies + run: forge install - name: Run Tests run: yarn test eslint: @@ -122,6 +128,12 @@ jobs: cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile --network-concurrency 1 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Show Forge Version + run: forge --version + - name: Install Forge dependancies + run: forge install - name: Compile contracts run: yarn compile - name: Build dist files diff --git a/.gitignore b/.gitignore index 73909928..07017cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ package-lock.json # Hardhat files cache artifacts +cache_hardhat # Build files dist/ diff --git a/.gitmodules b/.gitmodules index d11ef0bb..87086272 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,14 @@ [submodule "lib/axelar-gmp-sdk-solidity"] path = lib/axelar-gmp-sdk-solidity url = https://github.com/axelarnetwork/axelar-gmp-sdk-solidity + +[submodule "lib/immutable-seaport-core-1.6.0+im2"] + path = lib/immutable-seaport-core-1.6.0+im2 + url = https://github.com/immutable/seaport-core + +[submodule "lib/immutable-seaport-1.6.0+im4"] + path = lib/immutable-seaport-1.6.0+im4 + url = https://github.com/immutable/seaport +[submodule "lib/creator-token-standards"] + path = lib/creator-token-standards + url = https://github.com/limitbreakinc/creator-token-standards diff --git a/DEPS.md b/DEPS.md new file mode 100644 index 00000000..5d92224d --- /dev/null +++ b/DEPS.md @@ -0,0 +1,21 @@ +# Dependency Configuration + +This repo uses the Foundry tool chain to build and test Solidity code. + +The instructions below were used to install the dependencies. + +``` +forge install https://github.com/estarriolvetch/solidity-bits --no-commit +forge install https://github.com/GNSPS/solidity-bytes-utils --co-commit +forge install https://github.com/axelarnetwork/axelar-gmp-sdk-solidity --co-commit + +forge install openzeppelin-contracts-4.9.3=OpenZeppelin/openzeppelin-contracts@4.9.3 --no-commit +forge install openzeppelin-contracts-upgradeable-4.9.3=OpenZeppelin/openzeppelin-contracts-upgradeable@4.9.3 --no-commit +forge install openzeppelin-contracts-5.0.2=OpenZeppelin/openzeppelin-contracts@5.0.2 --no-commit + +forge install immutable-seaport-1.5.0+im1.3=immutable/seaport@1.5.0+im1.3 --no-commit +forge install immutable-seaport-core-1.5.0+im1=immutable/seaport-core@1.5.0+im1 --no-commit + +forge install immutable-seaport-1.6.0+im1=immutable/seaport@1.6.0+im1 --no-commit +forge install immutable-seaport-core-1.6.0+im1=immutable/seaport-core@1.6.0+im1 --no-commit +``` diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol new file mode 100644 index 00000000..fb0d6973 --- /dev/null +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -0,0 +1,633 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache-2 +// solhint-disable compiler-version +pragma solidity ^0.8.24; + +import {Consideration} from "seaport-core-16/src/lib/Consideration.sol"; +import {AdvancedOrder, BasicOrderParameters, CriteriaResolver, Execution, Fulfillment, FulfillmentComponent, Order} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {OrderType} from "seaport-types-16/src/lib/ConsiderationEnums.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ImmutableSeaportEvents} from "./interfaces/ImmutableSeaportEvents.sol"; + +/** + * @title ImmutableSeaport + * @custom:version 1.6 + * @notice Seaport is a generalized native token/ERC20/ERC721/ERC1155 + * marketplace with lightweight methods for common routes as well as + * more flexible methods for composing advanced orders or groups of + * orders. Each order contains an arbitrary number of items that may be + * spent (the "offer") along with an arbitrary number of items that must + * be received back by the indicated recipients (the "consideration"). + */ +contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { + // Mapping to store valid ImmutableZones - this allows for multiple Zones + // to be active at the same time, and can be expired or added on demand. + // solhint-disable-next-line named-parameters-mapping + mapping(address => bool) public allowedZones; + + error OrderNotRestricted(); + error InvalidZone(address zone); + + /** + * @notice Derive and set hashes, reference chainId, and associated domain + * separator during deployment. + * + * @param conduitController A contract that deploys conduits, or proxies + * that may optionally be used to transfer approved + * ERC20/721/1155 tokens. + * @param owner The address of the owner of this contract. Specified in the + * constructor to be CREATE2 / CREATE3 compatible. + */ + constructor(address conduitController, address owner) Consideration(conduitController) Ownable() { + // Transfer ownership to the address specified in the constructor + _transferOwnership(owner); + } + + /** + * @dev Set the validity of a zone for use during fulfillment. + */ + function setAllowedZone(address zone, bool allowed) external onlyOwner { + allowedZones[zone] = allowed; + emit AllowedZoneSet(zone, allowed); + } + + /** + * @dev Internal pure function to retrieve and return the name of this + * contract. + * + * @return The name of this contract. + */ + function _name() internal pure override returns (string memory) { + // Return the name of the contract. + return "ImmutableSeaport"; + } + + /** + * @dev Internal pure function to retrieve the name of this contract as a + * string that will be used to derive the name hash in the constructor. + * + * @return The name of this contract as a string. + */ + // slither-disable-next-line dead-code + function _nameString() internal pure override returns (string memory) { + // Return the name of the contract. + return "ImmutableSeaport"; + } + + /** + * @dev Helper function to revert any fulfillment that has an invalid zone + */ + function _rejectIfZoneInvalid(address zone) internal view { + if (!allowedZones[zone]) { + revert InvalidZone(zone); + } + } + + /** + * @notice Fulfill an order offering an ERC20, ERC721, or ERC1155 item by + * supplying Ether (or other native tokens), ERC20 tokens, an ERC721 + * item, or an ERC1155 item as consideration. Six permutations are + * supported: Native token to ERC721, Native token to ERC1155, ERC20 + * to ERC721, ERC20 to ERC1155, ERC721 to ERC20, and ERC1155 to + * ERC20 (with native tokens supplied as msg.value). For an order to + * be eligible for fulfillment via this method, it must contain a + * single offer item (though that item may have a greater amount if + * the item is not an ERC721). An arbitrary number of "additional + * recipients" may also be supplied which will each receive native + * tokens or ERC20 items from the fulfiller as consideration. Refer + * to the documentation for a more comprehensive summary of how to + * utilize this method and what orders are compatible with it. + * + * @param parameters Additional information on the fulfilled order. Note + * that the offerer and the fulfiller must first approve + * this contract (or their chosen conduit if indicated) + * before any tokens can be transferred. Also note that + * contract recipients of ERC1155 consideration items must + * implement `onERC1155Received` to receive those items. + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + function fulfillBasicOrder( + BasicOrderParameters calldata parameters + ) public payable virtual override returns (bool fulfilled) { + // All restricted orders are captured using this method + if (uint256(parameters.basicOrderType) % 4 != 2 && uint256(parameters.basicOrderType) % 4 != 3) { + revert OrderNotRestricted(); + } + + _rejectIfZoneInvalid(parameters.zone); + + return super.fulfillBasicOrder(parameters); + } + + /** + * @notice Fulfill an order offering an ERC20, ERC721, or ERC1155 item by + * supplying Ether (or other native tokens), ERC20 tokens, an ERC721 + * item, or an ERC1155 item as consideration. Six permutations are + * supported: Native token to ERC721, Native token to ERC1155, ERC20 + * to ERC721, ERC20 to ERC1155, ERC721 to ERC20, and ERC1155 to + * ERC20 (with native tokens supplied as msg.value). For an order to + * be eligible for fulfillment via this method, it must contain a + * single offer item (though that item may have a greater amount if + * the item is not an ERC721). An arbitrary number of "additional + * recipients" may also be supplied which will each receive native + * tokens or ERC20 items from the fulfiller as consideration. Refer + * to the documentation for a more comprehensive summary of how to + * utilize this method and what orders are compatible with it. Note + * that this function costs less gas than `fulfillBasicOrder` due to + * the zero bytes in the function selector (0x00000000) which also + * results in earlier function dispatch. + * + * @param parameters Additional information on the fulfilled order. Note + * that the offerer and the fulfiller must first approve + * this contract (or their chosen conduit if indicated) + * before any tokens can be transferred. Also note that + * contract recipients of ERC1155 consideration items must + * implement `onERC1155Received` to receive those items. + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + // solhint-disable-next-line func-name-mixedcase + function fulfillBasicOrder_efficient_6GL6yc( + BasicOrderParameters calldata parameters + ) public payable virtual override returns (bool fulfilled) { + // All restricted orders are captured using this method + if (uint256(parameters.basicOrderType) % 4 != 2 && uint256(parameters.basicOrderType) % 4 != 3) { + revert OrderNotRestricted(); + } + + _rejectIfZoneInvalid(parameters.zone); + + return super.fulfillBasicOrder_efficient_6GL6yc(parameters); + } + + /** + * @notice Fulfill an order with an arbitrary number of items for offer and + * consideration. Note that this function does not support + * criteria-based orders or partial filling of orders (though + * filling the remainder of a partially-filled order is supported). + * + * @custom:param order The order to fulfill. Note that both the + * offerer and the fulfiller must first approve + * this contract (or the corresponding conduit if + * indicated) to transfer any relevant tokens on + * their behalf and that contracts must implement + * `onERC1155Received` to receive ERC1155 tokens + * as consideration. + * @param fulfillerConduitKey A bytes32 value indicating what conduit, if + * any, to source the fulfiller's token approvals + * from. The zero hash signifies that no conduit + * should be used (and direct approvals set on + * this contract). + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + function fulfillOrder( + /** + * @custom:name order + */ + Order calldata order, + bytes32 fulfillerConduitKey + ) public payable virtual override returns (bool fulfilled) { + if ( + order.parameters.orderType != OrderType.FULL_RESTRICTED && + order.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + + _rejectIfZoneInvalid(order.parameters.zone); + + return super.fulfillOrder(order, fulfillerConduitKey); + } + + /** + * @notice Fill an order, fully or partially, with an arbitrary number of + * items for offer and consideration alongside criteria resolvers + * containing specific token identifiers and associated proofs. + * + * @custom:param advancedOrder The order to fulfill along with the + * fraction of the order to attempt to fill. + * Note that both the offerer and the + * fulfiller must first approve this + * contract (or their conduit if indicated + * by the order) to transfer any relevant + * tokens on their behalf and that contracts + * must implement `onERC1155Received` to + * receive ERC1155 tokens as consideration. + * Also note that all offer and + * consideration components must have no + * remainder after multiplication of the + * respective amount with the supplied + * fraction for the partial fill to be + * considered valid. + * @custom:param criteriaResolvers An array where each element contains a + * reference to a specific offer or + * consideration, a token identifier, and a + * proof that the supplied token identifier + * is contained in the merkle root held by + * the item in question's criteria element. + * Note that an empty criteria indicates + * that any (transferable) token identifier + * on the token in question is valid and + * that no associated proof needs to be + * supplied. + * @param fulfillerConduitKey A bytes32 value indicating what conduit, + * if any, to source the fulfiller's token + * approvals from. The zero hash signifies + * that no conduit should be used (and + * direct approvals set on this contract). + * @param recipient The intended recipient for all received + * items, with `address(0)` indicating that + * the caller should receive the items. + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + function fulfillAdvancedOrder( + /** + * @custom:name advancedOrder + */ + AdvancedOrder calldata advancedOrder, + /** + * @custom:name criteriaResolvers + */ + CriteriaResolver[] calldata criteriaResolvers, + bytes32 fulfillerConduitKey, + address recipient + ) public payable virtual override returns (bool fulfilled) { + if ( + advancedOrder.parameters.orderType != OrderType.FULL_RESTRICTED && + advancedOrder.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + + _rejectIfZoneInvalid(advancedOrder.parameters.zone); + + return super.fulfillAdvancedOrder(advancedOrder, criteriaResolvers, fulfillerConduitKey, recipient); + } + + /** + * @notice Attempt to fill a group of orders, each with an arbitrary number + * of items for offer and consideration. Any order that is not + * currently active, has already been fully filled, or has been + * cancelled will be omitted. Remaining offer and consideration + * items will then be aggregated where possible as indicated by the + * supplied offer and consideration component arrays and aggregated + * items will be transferred to the fulfiller or to each intended + * recipient, respectively. Note that a failing item transfer or an + * issue with order formatting will cause the entire batch to fail. + * Note that this function does not support criteria-based orders or + * partial filling of orders (though filling the remainder of a + * partially-filled order is supported). + * + * @custom:param orders The orders to fulfill. Note that + * both the offerer and the + * fulfiller must first approve this + * contract (or the corresponding + * conduit if indicated) to transfer + * any relevant tokens on their + * behalf and that contracts must + * implement `onERC1155Received` to + * receive ERC1155 tokens as + * consideration. + * @custom:param offerFulfillments An array of FulfillmentComponent + * arrays indicating which offer + * items to attempt to aggregate + * when preparing executions. Note + * that any offer items not included + * as part of a fulfillment will be + * sent unaggregated to the caller. + * @custom:param considerationFulfillments An array of FulfillmentComponent + * arrays indicating which + * consideration items to attempt to + * aggregate when preparing + * executions. + * @param fulfillerConduitKey A bytes32 value indicating what + * conduit, if any, to source the + * fulfiller's token approvals from. + * The zero hash signifies that no + * conduit should be used (and + * direct approvals set on this + * contract). + * @param maximumFulfilled The maximum number of orders to + * fulfill. + * + * @return availableOrders An array of booleans indicating if each order + * with an index corresponding to the index of the + * returned boolean was fulfillable or not. + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. + */ + function fulfillAvailableOrders( + /** + * @custom:name orders + */ + Order[] calldata orders, + /** + * @custom:name offerFulfillments + */ + FulfillmentComponent[][] calldata offerFulfillments, + /** + * @custom:name considerationFulfillments + */ + FulfillmentComponent[][] calldata considerationFulfillments, + bytes32 fulfillerConduitKey, + uint256 maximumFulfilled + ) + public + payable + virtual + override + returns (bool[] memory, /* availableOrders */ Execution[] memory /* executions */) + { + for (uint256 i = 0; i < orders.length; i++) { + Order memory order = orders[i]; + if ( + order.parameters.orderType != OrderType.FULL_RESTRICTED && + order.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + _rejectIfZoneInvalid(order.parameters.zone); + } + + return + super.fulfillAvailableOrders( + orders, + offerFulfillments, + considerationFulfillments, + fulfillerConduitKey, + maximumFulfilled + ); + } + + /** + * @notice Attempt to fill a group of orders, fully or partially, with an + * arbitrary number of items for offer and consideration per order + * alongside criteria resolvers containing specific token + * identifiers and associated proofs. Any order that is not + * currently active, has already been fully filled, or has been + * cancelled will be omitted. Remaining offer and consideration + * items will then be aggregated where possible as indicated by the + * supplied offer and consideration component arrays and aggregated + * items will be transferred to the fulfiller or to each intended + * recipient, respectively. Note that a failing item transfer or an + * issue with order formatting will cause the entire batch to fail. + * + * @custom:param advancedOrders The orders to fulfill along with + * the fraction of those orders to + * attempt to fill. Note that both + * the offerer and the fulfiller + * must first approve this contract + * (or their conduit if indicated by + * the order) to transfer any + * relevant tokens on their behalf + * and that contracts must implement + * `onERC1155Received` to receive + * ERC1155 tokens as consideration. + * Also note that all offer and + * consideration components must + * have no remainder after + * multiplication of the respective + * amount with the supplied fraction + * for an order's partial fill + * amount to be considered valid. + * @custom:param criteriaResolvers An array where each element + * contains a reference to a + * specific offer or consideration, + * a token identifier, and a proof + * that the supplied token + * identifier is contained in the + * merkle root held by the item in + * question's criteria element. Note + * that an empty criteria indicates + * that any (transferable) token + * identifier on the token in + * question is valid and that no + * associated proof needs to be + * supplied. + * @custom:param offerFulfillments An array of FulfillmentComponent + * arrays indicating which offer + * items to attempt to aggregate + * when preparing executions. Note + * that any offer items not included + * as part of a fulfillment will be + * sent unaggregated to the caller. + * @custom:param considerationFulfillments An array of FulfillmentComponent + * arrays indicating which + * consideration items to attempt to + * aggregate when preparing + * executions. + * @param fulfillerConduitKey A bytes32 value indicating what + * conduit, if any, to source the + * fulfiller's token approvals from. + * The zero hash signifies that no + * conduit should be used (and + * direct approvals set on this + * contract). + * @param recipient The intended recipient for all + * received items, with `address(0)` + * indicating that the caller should + * receive the offer items. + * @param maximumFulfilled The maximum number of orders to + * fulfill. + * + * @return availableOrders An array of booleans indicating if each order + * with an index corresponding to the index of the + * returned boolean was fulfillable or not. + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. + */ + function fulfillAvailableAdvancedOrders( + /** + * @custom:name advancedOrders + */ + AdvancedOrder[] calldata advancedOrders, + /** + * @custom:name criteriaResolvers + */ + CriteriaResolver[] calldata criteriaResolvers, + /** + * @custom:name offerFulfillments + */ + FulfillmentComponent[][] calldata offerFulfillments, + /** + * @custom:name considerationFulfillments + */ + FulfillmentComponent[][] calldata considerationFulfillments, + bytes32 fulfillerConduitKey, + address recipient, + uint256 maximumFulfilled + ) + public + payable + virtual + override + returns (bool[] memory, /* availableOrders */ Execution[] memory /* executions */) + { + for (uint256 i = 0; i < advancedOrders.length; i++) { + AdvancedOrder memory advancedOrder = advancedOrders[i]; + if ( + advancedOrder.parameters.orderType != OrderType.FULL_RESTRICTED && + advancedOrder.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + + _rejectIfZoneInvalid(advancedOrder.parameters.zone); + } + + return + super.fulfillAvailableAdvancedOrders( + advancedOrders, + criteriaResolvers, + offerFulfillments, + considerationFulfillments, + fulfillerConduitKey, + recipient, + maximumFulfilled + ); + } + + /** + * @notice Match an arbitrary number of orders, each with an arbitrary + * number of items for offer and consideration along with a set of + * fulfillments allocating offer components to consideration + * components. Note that this function does not support + * criteria-based or partial filling of orders (though filling the + * remainder of a partially-filled order is supported). Any unspent + * offer item amounts or native tokens will be transferred to the + * caller. + * + * @custom:param orders The orders to match. Note that both the + * offerer and fulfiller on each order must first + * approve this contract (or their conduit if + * indicated by the order) to transfer any + * relevant tokens on their behalf and each + * consideration recipient must implement + * `onERC1155Received` to receive ERC1155 tokens. + * @custom:param fulfillments An array of elements allocating offer + * components to consideration components. Note + * that each consideration component must be + * fully met for the match operation to be valid, + * and that any unspent offer items will be sent + * unaggregated to the caller. + * + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. Note that unspent offer item amounts or native + * tokens will not be reflected as part of this array. + */ + function matchOrders( + /** + * @custom:name orders + */ + Order[] calldata orders, + /** + * @custom:name fulfillments + */ + Fulfillment[] calldata fulfillments + ) public payable virtual override returns (Execution[] memory /* executions */) { + for (uint256 i = 0; i < orders.length; i++) { + Order memory order = orders[i]; + if ( + order.parameters.orderType != OrderType.FULL_RESTRICTED && + order.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + _rejectIfZoneInvalid(order.parameters.zone); + } + + return super.matchOrders(orders, fulfillments); + } + + /** + * @notice Match an arbitrary number of full, partial, or contract orders, + * each with an arbitrary number of items for offer and + * consideration, supplying criteria resolvers containing specific + * token identifiers and associated proofs as well as fulfillments + * allocating offer components to consideration components. Any + * unspent offer item amounts will be transferred to the designated + * recipient (with the null address signifying to use the caller) + * and any unspent native tokens will be returned to the caller. + * + * @custom:param advancedOrders The advanced orders to match. Note that + * both the offerer and fulfiller on each + * order must first approve this contract + * (or their conduit if indicated by the + * order) to transfer any relevant tokens on + * their behalf and each consideration + * recipient must implement + * `onERC1155Received` to receive ERC1155 + * tokens. Also note that the offer and + * consideration components for each order + * must have no remainder after multiplying + * the respective amount with the supplied + * fraction for the group of partial fills + * to be considered valid. + * @custom:param criteriaResolvers An array where each element contains a + * reference to a specific offer or + * consideration, a token identifier, and a + * proof that the supplied token identifier + * is contained in the merkle root held by + * the item in question's criteria element. + * Note that an empty criteria indicates + * that any (transferable) token identifier + * on the token in question is valid and + * that no associated proof needs to be + * supplied. + * @custom:param fulfillments An array of elements allocating offer + * components to consideration components. + * Note that each consideration component + * must be fully met for the match operation + * to be valid, and that any unspent offer + * items will be sent unaggregated to the + * designated recipient. + * @param recipient The intended recipient for all unspent + * offer item amounts, or the caller if the + * null address is supplied. + * + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. Note that unspent offer item amounts or + * native tokens will not be reflected as part of this + * array. + */ + function matchAdvancedOrders( + /** + * @custom:name advancedOrders + */ + AdvancedOrder[] calldata advancedOrders, + /** + * @custom:name criteriaResolvers + */ + CriteriaResolver[] calldata criteriaResolvers, + /** + * @custom:name fulfillments + */ + Fulfillment[] calldata fulfillments, + address recipient + ) public payable virtual override returns (Execution[] memory /* executions */) { + for (uint256 i = 0; i < advancedOrders.length; i++) { + AdvancedOrder memory advancedOrder = advancedOrders[i]; + if ( + advancedOrder.parameters.orderType != OrderType.FULL_RESTRICTED && + advancedOrder.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + + _rejectIfZoneInvalid(advancedOrder.parameters.zone); + } + + return super.matchAdvancedOrders(advancedOrders, criteriaResolvers, fulfillments, recipient); + } +} diff --git a/contracts/trading/seaport16/conduit/ConduitController.sol b/contracts/trading/seaport16/conduit/ConduitController.sol new file mode 100644 index 00000000..5cd920aa --- /dev/null +++ b/contracts/trading/seaport16/conduit/ConduitController.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +// solhint-disable +pragma solidity ^0.8.17; + +import {ConduitController} from "seaport-core-16/src/conduit/ConduitController.sol"; diff --git a/contracts/trading/seaport16/interfaces/ImmutableSeaportEvents.sol b/contracts/trading/seaport16/interfaces/ImmutableSeaportEvents.sol new file mode 100644 index 00000000..c3896fea --- /dev/null +++ b/contracts/trading/seaport16/interfaces/ImmutableSeaportEvents.sol @@ -0,0 +1,15 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache-2 +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +/** + * @notice ImmutableSeaportEvents contains events + * related to the ImmutableSeaport contract + */ +interface ImmutableSeaportEvents { + /** + * @dev Emit an event when an allowed zone status is updated + */ + event AllowedZoneSet(address zoneAddress, bool allowed); +} diff --git a/contracts/trading/seaport16/validators/ReadOnlyOrderValidator.sol b/contracts/trading/seaport16/validators/ReadOnlyOrderValidator.sol new file mode 100644 index 00000000..74de5531 --- /dev/null +++ b/contracts/trading/seaport16/validators/ReadOnlyOrderValidator.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +// solhint-disable +pragma solidity ^0.8.17; + +import {ReadOnlyOrderValidator} from "seaport-16/contracts/helpers/order-validator/lib/ReadOnlyOrderValidator.sol"; diff --git a/contracts/trading/seaport16/validators/SeaportValidator.sol b/contracts/trading/seaport16/validators/SeaportValidator.sol new file mode 100644 index 00000000..196db81b --- /dev/null +++ b/contracts/trading/seaport16/validators/SeaportValidator.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +// solhint-disable +pragma solidity ^0.8.17; + +import {SeaportValidator} from "seaport-16/contracts/helpers/order-validator/SeaportValidator.sol"; diff --git a/contracts/trading/seaport16/validators/SeaportValidatorHelper.sol b/contracts/trading/seaport16/validators/SeaportValidatorHelper.sol new file mode 100644 index 00000000..8f565398 --- /dev/null +++ b/contracts/trading/seaport16/validators/SeaportValidatorHelper.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +// solhint-disable +pragma solidity ^0.8.17; + +import {SeaportValidatorHelper} from "seaport-16/contracts/helpers/order-validator/lib/SeaportValidatorHelper.sol"; diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol new file mode 100644 index 00000000..7f3ed8e8 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -0,0 +1,880 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.20; + +import {ITransferValidator} from "@limitbreak/creator-token-standards/src/interfaces/ITransferValidator.sol"; +import {AccessControlEnumerable} from "openzeppelin-contracts-5.0.2/access/extensions/AccessControlEnumerable.sol"; +import {ECDSA} from "openzeppelin-contracts-5.0.2/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "openzeppelin-contracts-5.0.2/utils/cryptography/MessageHashUtils.sol"; +import {ERC165} from "openzeppelin-contracts-5.0.2/utils/introspection/ERC165.sol"; +import {Math} from "openzeppelin-contracts-5.0.2/utils/math/Math.sol"; +import {ZoneInterface} from "seaport-types-16/src/interfaces/ZoneInterface.sol"; +import {ZoneParameters, Schema, ReceivedItem} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {ZoneAccessControl} from "./ZoneAccessControl.sol"; +import {SIP5Interface} from "./interfaces/SIP5Interface.sol"; +import {SIP6Interface} from "./interfaces/SIP6Interface.sol"; +import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; + +/** + * @title ImmutableSignedZoneV3 + * @author Immutable + * @notice ImmutableSignedZoneV3 is a zone implementation based on the + * SIP-7 standard https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md + * implementing substandards 1, 3, 4, 6, 7 and 8. + * + * The contract is not upgradable. If the contract needs to be changed a new version + * should be deployed, and the old version should be removed from the Seaport contract + * zone allowlist. + */ +contract ImmutableSignedZoneV3 is + ERC165, + ZoneAccessControl, + ZoneInterface, + SIP5Interface, + SIP6Interface, + SIP7Interface +{ + /// @dev The EIP-712 domain type hash. + bytes32 private constant _EIP_712_DOMAIN_TYPEHASH = + keccak256( + abi.encodePacked( + "EIP712Domain(", + "string name,", + "string version,", + "uint256 chainId,", + "address verifyingContract", + ")" + ) + ); + + /// @dev The EIP-712 domain version value. + bytes32 private constant _VERSION_HASH = keccak256(bytes("3.0")); + + /// @dev The EIP-712 signed order type hash. + bytes32 private constant _SIGNED_ORDER_TYPEHASH = + keccak256( + abi.encodePacked( + "SignedOrder(", + "address fulfiller,", + "uint64 expiration,", + "bytes32 orderHash,", + "bytes context", + ")" + ) + ); + + /// @dev The chain ID on which the contract was deployed. + uint256 private immutable _CHAIN_ID = block.chainid; + + /// @dev The domain separator used for signing. + bytes32 private immutable _DOMAIN_SEPARATOR; + + /// @dev The accepted SIP-6 version. + uint8 private constant _ACCEPTED_SIP6_VERSION = 0; + + /// @dev The name for this zone returned in getSeaportMetadata(). + // solhint-disable-next-line var-name-mixedcase + string private _ZONE_NAME; + + bytes32 private immutable _NAME_HASH; + + /// @dev The Seaport contract address. + address private immutable _SEAPORT; + + /// @dev The allowed signers. + // solhint-disable-next-line named-parameters-mapping + mapping(address => SignerInfo) private _signers; + + /// @dev The API endpoint where orders for this zone can be signed. + string private _apiEndpoint; + + /// @dev The documentationURI. + string private _documentationURI; + + /** + * @notice Constructor to deploy the contract. + * + * @param zoneName The name for the zone returned in getSeaportMetadata(). + * @param seaport The Seaport contract address. + * @param apiEndpoint The API endpoint where orders for this zone can be signed. + * Request and response payloads are defined in SIP-7. + * @param documentationURI The documentation URI. + * @param owner The address of the owner of this contract. Specified in the + * constructor to be CREATE2 / CREATE3 compatible. + */ + constructor( + string memory zoneName, + address seaport, + string memory apiEndpoint, + string memory documentationURI, + address owner + ) ZoneAccessControl(owner) { + // Set the zone name. + _ZONE_NAME = zoneName; + + // Set name hash. + _NAME_HASH = keccak256(bytes(zoneName)); + + // Set the Seaport contract address. + if (seaport == address(0)) { + revert SeaportCannotBeZeroAddress(); + } + _SEAPORT = seaport; + + // Set the API endpoint. + _apiEndpoint = apiEndpoint; + + // Set the documentation URI. + _documentationURI = documentationURI; + + // Derive and set the domain separator. + _DOMAIN_SEPARATOR = _deriveDomainSeparator(); + + // Emit an event to signal a SIP-5 contract has been deployed. + emit SeaportCompatibleContractDeployed(); + } + + /** + * @notice Add a new signer to the zone. + * + * @param signer The new signer address to add. + */ + function addSigner(address signer) external override onlyRole(ZONE_MANAGER_ROLE) { + // Do not allow the zero address to be added as a signer. + if (signer == address(0)) { + revert SignerCannotBeZeroAddress(); + } + + // Revert if the signer is already active. + if (_signers[signer].active) { + revert SignerAlreadyActive(signer); + } + + // Revert if the signer was previously authorized. + // Specified in SIP-7 to prevent compromised signer from being + // cycled back into use. + if (_signers[signer].previouslyActive) { + revert SignerCannotBeReauthorized(signer); + } + + // Set the signer info. + _signers[signer] = SignerInfo(true, true); + + // Emit an event that the signer was added. + emit SignerAdded(signer); + } + + /** + * @notice Remove an active signer from the zone. + * + * @param signer The signer address to remove. + */ + function removeSigner(address signer) external override onlyRole(ZONE_MANAGER_ROLE) { + // Revert if the signer is not active. + if (!_signers[signer].active) { + revert SignerNotActive(signer); + } + + // Set the signer's active status to false. + _signers[signer].active = false; + + // Emit an event that the signer was removed. + emit SignerRemoved(signer); + } + + /** + * @notice Update the API endpoint returned by this zone. + * + * @param newApiEndpoint The new API endpoint. + */ + function updateAPIEndpoint(string calldata newApiEndpoint) external override onlyRole(ZONE_MANAGER_ROLE) { + _apiEndpoint = newApiEndpoint; + } + + /** + * @notice Update the documentation URI returned by this zone. + * + * @param newDocumentationURI The new documentation URI. + */ + function updateDocumentationURI(string calldata newDocumentationURI) external override onlyRole(ZONE_MANAGER_ROLE) { + _documentationURI = newDocumentationURI; + } + + /** + * @dev Returns Seaport metadata for this contract, returning the + * contract name and supported schemas. + * + * @return name The contract name. + * @return schemas The supported SIPs. + */ + function getSeaportMetadata() + external + view + override(SIP5Interface, ZoneInterface) + returns (string memory name, Schema[] memory schemas) + { + name = _ZONE_NAME; + + // supported SIP (7) + schemas = new Schema[](1); + schemas[0].id = 7; + schemas[0].metadata = abi.encode( + _domainSeparator(), + _apiEndpoint, + _getSupportedSubstandards(), + _documentationURI + ); + } + + /** + * @notice Returns signing information about the zone. + * + * @return domainSeparator The domain separator used for signing. + * @return apiEndpoint The API endpoint to get signatures for orders. + * @return substandards The supported substandards. + * @return documentationURI The documentation URI. + */ + function sip7Information() + external + view + override + returns ( + bytes32 domainSeparator, + string memory apiEndpoint, + uint256[] memory substandards, + string memory documentationURI + ) + { + domainSeparator = _domainSeparator(); + apiEndpoint = _apiEndpoint; + + substandards = _getSupportedSubstandards(); + + documentationURI = _documentationURI; + } + + /** + * @notice Check if a given order including extraData is currently valid. + * + * @dev This function is called by Seaport whenever any extraData is + * provided by the caller, before tokens have been transferred. + * + * @param zoneParameters The zone parameters containing data related to + * the fulfilment execution. + * @return authorizedOrderMagicValue A magic value indicating if the order + * is currently valid. + */ + function authorizeOrder( + ZoneParameters calldata zoneParameters + ) external override returns (bytes4 authorizedOrderMagicValue) { + // Revert if the caller is not the Seaport contract. + if (msg.sender != _SEAPORT) { + revert CallerNotSeaport(); + } + + // Put the extraData and orderHash on the stack for cheaper access. + bytes calldata extraData = zoneParameters.extraData; + bytes32 orderHash = zoneParameters.orderHash; + + // Revert with an error if the extraData is empty. + if (extraData.length == 0) { + revert InvalidExtraData("extraData is empty", orderHash); + } + + // We expect the extraData to conform with SIP-6 as well as SIP-7 + // Therefore all SIP-7 related data is offset by one byte + // SIP-7 specifically requires SIP-6 as a prerequisite. + + // Revert with an error if the extraData does not have valid length. + if (extraData.length < 93) { + revert InvalidExtraData("extraData length must be at least 93 bytes", orderHash); + } + + // Revert if SIP-6 version is not accepted (0). + if (uint8(extraData[0]) != _ACCEPTED_SIP6_VERSION) { + revert UnsupportedExtraDataVersion(uint8(extraData[0])); + } + + // extraData bytes 1-20: expected fulfiller. + // (zero address means not restricted). + address expectedFulfiller = address(bytes20(extraData[1:21])); + + // extraData bytes 21-28: expiration timestamp. + uint64 expiration = uint64(bytes8(extraData[21:29])); + + // extraData bytes 29-92: signature. + // (strictly requires 64 byte compact sig, ERC2098). + bytes calldata signature = extraData[29:93]; + + // extraData bytes 93-end: context (optional, variable length). + bytes calldata context = extraData[93:]; + + // Revert if expired. + // solhint-disable-next-line not-rely-on-time + if (block.timestamp > expiration) { + // solhint-disable-next-line not-rely-on-time + revert SignatureExpired(block.timestamp, expiration, orderHash); + } + + // Put fulfiller on the stack for more efficient access. + address actualFulfiller = zoneParameters.fulfiller; + + // Revert unless: + // - expected fulfiller is 0 address (any fulfiller) OR + // - expected fulfiller is the same as actual fulfiller. + if (expectedFulfiller != address(0) && expectedFulfiller != actualFulfiller) { + revert InvalidFulfiller(expectedFulfiller, actualFulfiller, orderHash); + } + + // Revert if no spent items are provided. + if (zoneParameters.offer.length == 0) { + revert NoSpentItems(orderHash); + } + + // Revert if no received items are provided. + if (zoneParameters.consideration.length == 0) { + revert NoReceivedItems(orderHash); + } + + // Validate supported substandards - before hook validation. + _validateSubstandards(context, zoneParameters, true); + + // Derive the signedOrder hash. + bytes32 signedOrderHash = _deriveSignedOrderHash(expectedFulfiller, expiration, orderHash, context); + + // Derive the EIP-712 digest using the domain separator and signedOrder + // hash through openzepplin helper. + bytes32 digest = MessageHashUtils.toTypedDataHash(_domainSeparator(), signedOrderHash); + + // Recover the signer address from the digest and signature. + // Pass in R and VS from compact signature (ERC2098). + address recoveredSigner = ECDSA.recover(digest, bytes32(signature[0:32]), bytes32(signature[32:64])); + + // Revert if the signer is not active. + // This also reverts if the digest constructed on serverside is incorrect. + if (!_signers[recoveredSigner].active) { + revert SignerNotActive(recoveredSigner); + } + + // Pre hook validation completes and passes with no reverts, return valid. + authorizedOrderMagicValue = ZoneInterface.authorizeOrder.selector; + } + + /** + * @notice Validates a fulfilment execution. + * + * @dev This function is called by Seaport whenever any extraData is + * provided by the caller, after tokens have been transferred. Note + * that this function omits redundant validation that is already performed + * in the authorizeOrder function which is called by Seaport before + * tokens are transferred. + * + * @param zoneParameters The zone parameters containing data related to + * the fulfilment execution. + * @return validOrderMagicValue A magic value indicating if the order is + * currently valid. + */ + function validateOrder( + ZoneParameters calldata zoneParameters + ) external override returns (bytes4 validOrderMagicValue) { + // Revert if the caller is not the Seaport contract. + if (msg.sender != _SEAPORT) { + revert CallerNotSeaport(); + } + + // Put the extraData and orderHash on the stack for cheaper access. + bytes calldata extraData = zoneParameters.extraData; + + // extraData bytes 93-end: context (optional, variable length). + // extraData length is guaranteed by the authorizeOrder function. + bytes calldata context = extraData[93:]; + + // Validate supported substandards - after hook validation. + _validateSubstandards(context, zoneParameters, false); + + // Pre hook validation completes and passes with no reverts, return valid. + validOrderMagicValue = ZoneInterface.validateOrder.selector; + } + + /** + * @notice ERC-165 interface support. + * + * @param interfaceId The interface ID to check for support. + */ + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC165, ZoneInterface, AccessControlEnumerable) returns (bool) { + return + interfaceId == type(ZoneInterface).interfaceId || + interfaceId == type(SIP5Interface).interfaceId || + interfaceId == type(SIP7Interface).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev Internal view function to get the EIP-712 domain separator. If the + * chainId matches the chainId set on deployment, the cached domain + * separator will be returned; otherwise, it will be derived from + * scratch. + * + * @return The domain separator. + */ + function _domainSeparator() internal view returns (bytes32) { + return block.chainid == _CHAIN_ID ? _DOMAIN_SEPARATOR : _deriveDomainSeparator(); + } + + /** + * @dev Internal view function to derive the EIP-712 domain separator. + * + * @return domainSeparator The derived domain separator. + */ + function _deriveDomainSeparator() internal view returns (bytes32 domainSeparator) { + return keccak256(abi.encode(_EIP_712_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this))); + } + + /** + * @dev Get the supported substandards of the contract. + * + * @return substandards Array of substandards supported. + */ + function _getSupportedSubstandards() internal pure returns (uint256[] memory substandards) { + // support substandards 1, 3, 4, 6, 7 and 8 + substandards = new uint256[](6); + substandards[0] = 1; + substandards[1] = 3; + substandards[2] = 4; + substandards[3] = 6; + substandards[4] = 7; + substandards[5] = 8; + } + + /** + * @dev Derive the signedOrder hash from the orderHash and expiration. + * + * @param fulfiller The expected fulfiller address. + * @param expiration The signature expiration timestamp. + * @param orderHash The order hash. + * @param context The optional variable-length context. + * @return signedOrderHash The signedOrder hash. + */ + function _deriveSignedOrderHash( + address fulfiller, + uint64 expiration, + bytes32 orderHash, + bytes calldata context + ) internal pure returns (bytes32 signedOrderHash) { + // Derive the signed order hash. + signedOrderHash = keccak256( + abi.encode(_SIGNED_ORDER_TYPEHASH, fulfiller, expiration, orderHash, keccak256(context)) + ); + } + + /** + * @dev Validate substandards 1, 3, 4, 6, 7 and 8 based on context. + * + * @param context Bytes payload of context. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + */ + function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) internal { + uint256 startIndex = 0; + uint256 contextLength = context.length; + + // The ImmutableSignedZoneV3 contract enforces at least + // one of the supported substandards is present in the context. + if (contextLength == 0) { + revert InvalidExtraData("invalid context, no substandards present", zoneParameters.orderHash); + } + + // Each _validateSubstandard* function returns the length of the substandard + // segment (0 if the substandard was not matched). + startIndex = _validateSubstandard1(context[startIndex:], zoneParameters, before) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard3(context[startIndex:], zoneParameters, before) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard4(context[startIndex:], zoneParameters, before) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard6(context[startIndex:], zoneParameters, before) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard7(context[startIndex:], zoneParameters, before) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard8(context[startIndex:], zoneParameters, before) + startIndex; + + if (startIndex != contextLength) { + revert InvalidExtraData("invalid context, unexpected context length", zoneParameters.orderHash); + } + } + + /** + * @dev Validates substandard 1. This substandard is used to validate that the server's + * specified first received item identifier matches the actual first received + * item identifier. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + * @return Length of substandard segment. + */ + function _validateSubstandard1( + bytes calldata context, + ZoneParameters calldata zoneParameters, + bool before + ) internal pure returns (uint256) { + if (uint8(context[0]) != 1) { + return 0; + } + + if (context.length < 33) { + revert InvalidExtraData("invalid substandard 1 data length", zoneParameters.orderHash); + } + + // Only perform validation in before hook. + if (before) { + // zoneParameters.consideration.length >= 1 is guaranteed by the authorizeOrder function. + if (uint256(bytes32(context[1:33])) != zoneParameters.consideration[0].identifier) { + revert Substandard1Violation(zoneParameters.orderHash, zoneParameters.consideration[0].identifier, uint256(bytes32(context[1:33]))); + } + } + + return 33; + } + + /** + * @dev Validates substandard 3. This substandard is used to validate that the server's + * specified received items matches the actual received items. This substandard + * should be used when the server is able to accurately determine the received items + * for an exact fulfilment scenario. This substandard should NOT be used for fulfilments + * where the received items cannot be accurately known in advance, as is the case in + * best-efforts partial fulfilment scenarios. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + * @return Length of substandard segment. + */ + function _validateSubstandard3( + bytes calldata context, + ZoneParameters calldata zoneParameters, + bool before + ) internal pure returns (uint256) { + if (uint8(context[0]) != 3) { + return 0; + } + + if (context.length < 33) { + revert InvalidExtraData("invalid substandard 3 data length", zoneParameters.orderHash); + } + + // Only perform validation in before hook. + if (before) { + if (_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1) != bytes32(context[1:33])) { + revert Substandard3Violation(zoneParameters.orderHash); + } + } + + return 33; + } + + /** + * @dev Validates substandard 4. This substandard is used to validate that the server's + * specified orders that must be bundled with the fulfilment are present. This is useful + * for scenarios where the fulfiller desires a bundled fulfilment to revert if part of + * bundle is not available for fulfilment. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + * @return Length of substandard segment. + */ + function _validateSubstandard4( + bytes calldata context, + ZoneParameters calldata zoneParameters, + bool before + ) internal pure returns (uint256) { + if (uint8(context[0]) != 4) { + return 0; + } + + // substandard ID + array offset + array length. + if (context.length < 65) { + revert InvalidExtraData("invalid substandard 4 data length", zoneParameters.orderHash); + } + + uint256 expectedOrderHashesSize = uint256(bytes32(context[33:65])); + uint256 substandardIndexEnd = 64 + (expectedOrderHashesSize * 32); + + // substandard ID + array offset + array length + array data. + if (context.length < substandardIndexEnd + 1) { + revert InvalidExtraData("invalid substandard 4 data length", zoneParameters.orderHash); + } + + // Only perform validation in after hook. Note that zoneParameters.orderHashes is only fully + // populated in the after hook (validateOrder call). + if (!before) { + bytes32[] memory expectedOrderHashes = abi.decode(context[1:substandardIndexEnd + 1], (bytes32[])); + + // revert if any order hashes in substandard data are not present in zoneParameters.orderHashes. + if (!_bytes32ArrayIncludes(zoneParameters.orderHashes, expectedOrderHashes)) { + revert Substandard4Violation(zoneParameters.orderHashes, expectedOrderHashes, zoneParameters.orderHash); + } + } + + return substandardIndexEnd + 1; + } + + /** + * @dev Validates substandard 6. This substandard a variation on substandard 3 to support + * that supports fulfilments where server cannot accurately determine expected received + * items in advance, as is the case in best-efforts partial fulfilment scenarios. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + * @return Length of substandard segment. + */ + function _validateSubstandard6( + bytes calldata context, + ZoneParameters calldata zoneParameters, + bool before + ) internal pure returns (uint256) { + if (uint8(context[0]) != 6) { + return 0; + } + + if (context.length < 65) { + revert InvalidExtraData("invalid substandard 6 data length", zoneParameters.orderHash); + } + + // Only perform validation in before hook. + if (before) { + // The first 32 bytes are the original first offer item amount. + uint256 originalFirstOfferItemAmount = uint256(bytes32(context[1:33])); + // The next 32 bytes are the hash of the received items that were expected + // derived based on an assumption of full fulfilment (i.e. numerator = denominator = 1). + bytes32 expectedReceivedItemsHash = bytes32(context[33:65]); + + // To support partial fulfilment scenarios, we must scale the actual received item amounts + // to match the expected received items hash based on full fulfilment (i.e. numerator = denominator = 1). + // + // actualAmount = originalAmount * numerator / denominator + // originalAmount = actualAmount * denominator / numerator + // + // The numerator and denominator values are inferred from the actual and original (extracted + // from context) amounts of the first offer item. + if ( + _deriveReceivedItemsHash( + zoneParameters.consideration, + originalFirstOfferItemAmount, + zoneParameters.offer[0].amount + ) != expectedReceivedItemsHash + ) { + revert Substandard6Violation( + zoneParameters.offer[0].amount, + originalFirstOfferItemAmount, + zoneParameters.orderHash + ); + } + } + + return 65; + } + + /** + * @dev Validates substandard 7. This substandard is a superset of substandard 1. It + * additionally calls the creator token standard transfer validator before and + * after token transfer hooks for a specified operator. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + * @return Length of substandard segment. + */ + function _validateSubstandard7( + bytes calldata context, + ZoneParameters calldata zoneParameters, + bool before + ) internal returns (uint256) { + if (uint8(context[0]) != 7) { + return 0; + } + + if (context.length < 73) { + revert InvalidExtraData("invalid substandard 7 data length", zoneParameters.orderHash); + } + + // Only perform identifier validation in before hook. + if (before) { + // zoneParameters.consideration.length >= 1 is guaranteed by the authorizeOrder function. + if (uint256(bytes32(context[1:33])) != zoneParameters.consideration[0].identifier) { + revert Substandard7IdentifierViolation(zoneParameters.orderHash, zoneParameters.consideration[0].identifier, uint256(bytes32(context[1:33]))); + } + } + + // This zone assumes that either the first consideration item or the first offer item is an ERC721 or ERC1155 token. + // slither-disable-next-line uninitialized-local + address token; + // zoneParameters.consideration.length >= 1 is guaranteed by the authorizeOrder function. + if (uint(zoneParameters.consideration[0].itemType) > 1) { + token = zoneParameters.consideration[0].token; + // zoneParameters.offer.length >= 1 is guaranteed by the authorizeOrder function. + } else if (uint(zoneParameters.offer[0].itemType) > 1) { + token = zoneParameters.offer[0].token; + } else { + revert Substandard7UnexpectedItemTypeViolation(zoneParameters.orderHash); + } + + address registry = address(bytes20(context[33:53])); + + if (before) { + ITransferValidator(registry).beforeAuthorizedTransfer(address(bytes20(context[53:73])), token); + } else { + ITransferValidator(registry).afterAuthorizedTransfer(token); + } + + return 73; + } + + /** + * @dev Validates substandard 8. This substandard is a superset of substandard 1. It + * additionally calls the creator token standard transfer validator before and + * after token transfer hooks. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @param before Whether validation is occurring in before or after hook. + * @return Length of substandard segment. + */ + function _validateSubstandard8( + bytes calldata context, + ZoneParameters calldata zoneParameters, + bool before + ) internal returns (uint256) { + if (uint8(context[0]) != 8) { + return 0; + } + + if (context.length < 53) { + revert InvalidExtraData("invalid substandard 8 data length", zoneParameters.orderHash); + } + + // Only perform identifier validation in before hook. + if (before) { + if (uint256(bytes32(context[1:33])) != zoneParameters.consideration[0].identifier) { + revert Substandard8IdentifierViolation(zoneParameters.orderHash, zoneParameters.consideration[0].identifier, uint256(bytes32(context[1:33]))); + } + } + + // This zone assumes that either the first consideration item or the first offer item is an ERC721 or ERC1155 token. + // slither-disable-next-line uninitialized-local + address token; + // slither-disable-next-line uninitialized-local + uint256 tokenId; + // zoneParameters.consideration.length >= 1 is guaranteed by the authorizeOrder function. + if (uint(zoneParameters.consideration[0].itemType) > 1) { + token = zoneParameters.consideration[0].token; + tokenId = zoneParameters.consideration[0].identifier; + // zoneParameters.offer.length >= 1 is guaranteed by the authorizeOrder function. + } else if (uint(zoneParameters.offer[0].itemType) > 1) { + token = zoneParameters.offer[0].token; + tokenId = zoneParameters.offer[0].identifier; + } else { + revert Substandard8UnexpectedItemTypeViolation(zoneParameters.orderHash); + } + + address registry = address(bytes20(context[33:53])); + + if (before) { + ITransferValidator(registry).beforeAuthorizedTransfer(token, tokenId); + } else { + ITransferValidator(registry).afterAuthorizedTransfer(token, tokenId); + } + + return 53; + } + + /** + * @dev Derive the received items hash based on received item array. + * + * @param receivedItems Actual received item array. + * @param scalingFactorNumerator Scaling factor numerator. + * @param scalingFactorDenominator Scaling factor denominator. + * @return receivedItemsHash Hash of received items. + */ + function _deriveReceivedItemsHash( + ReceivedItem[] calldata receivedItems, + uint256 scalingFactorNumerator, + uint256 scalingFactorDenominator + ) internal pure returns (bytes32) { + uint256 numberOfItems = receivedItems.length; + bytes memory receivedItemsHash = new bytes(0); // Explicitly initialize to empty bytes + + for (uint256 i; i < numberOfItems; i++) { + receivedItemsHash = abi.encodePacked( + receivedItemsHash, + receivedItems[i].itemType, + receivedItems[i].token, + receivedItems[i].identifier, + Math.mulDiv(receivedItems[i].amount, scalingFactorNumerator, scalingFactorDenominator), + receivedItems[i].recipient + ); + } + + return keccak256(receivedItemsHash); + } + + /** + * @dev Helper function to check if every element of values exists in sourceArray + * optimised for performance checking arrays sized 0-15. + * + * @param sourceArray Source array. + * @param values Values array. + * @return True if all elements in values exist in sourceArray. + */ + function _bytes32ArrayIncludes( + bytes32[] calldata sourceArray, + bytes32[] memory values + ) internal pure returns (bool) { + // cache the length in memory for loop optimisation + uint256 sourceArraySize = sourceArray.length; + uint256 valuesSize = values.length; + + // we can assume all items are unique + // therefore if values is bigger than superset sourceArray, return false + if (valuesSize > sourceArraySize) { + return false; + } + + // Iterate through each element and compare them + for (uint256 i = 0; i < valuesSize; ) { + bool found = false; + bytes32 item = values[i]; + for (uint256 j = 0; j < sourceArraySize; ) { + if (item == sourceArray[j]) { + // if item from values is in sourceArray, break + found = true; + break; + } + unchecked { + j++; + } + } + if (!found) { + // if any item from values is not found in sourceArray, return false + return false; + } + unchecked { + i++; + } + } + + // All elements from values exist in sourceArray + return true; + } +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md new file mode 100644 index 00000000..0e9ea585 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -0,0 +1,61 @@ +# Immutable Signed Zone (v3) + +The Immutable Signed Zone contract is a [Seaport Zone](https://docs.opensea.io/docs/seaport-hooks#zone-hooks) that implements [SIP-7 (Interface for Server-Signed Orders)](https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md) with support for [substandards](https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md#substandards) 1, 3, 4, 6, 7 and 8. + +This zone is used by Immutable to enable: + +* Enforcement of protocol, royalty and ecosystem fees +* Off-chain order cancellation + +# Status + +Contract threat models and audits: + +| Description | Date | Version Audited | Link to Report | +| -------------- | ---------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Threat Model | 2024-04-15 | V2 | [202404-threat-model-immutable-signed-zone-v2.md](../../../../../../audits/trading/202404-threat-model-immutable-signed-zone-v2.md) | +| Internal Audit | 2024-05-02 | V2 | [202405-internal-audit-immutable-signed-zone-v2.pdf](../../../../../../audits/trading/202405-internal-audit-immutable-signed-zone-v2.pdf) | + +## ImmutableSignedZoneV2 + +| Location | Date | Version Deployed | Address | +| ----------------------- | ------------ | ---------------- | ------- | +| Immutable zkEVM Testnet | Not deployed | - | - | +| Immutable zkEVM Mainnet | Not deployed | - | - | + +## Architecture + +The trading system on the Immutable platform is shown in the diagram below. + +```mermaid +flowchart LR + client[Client] <-- 1. POST .../fulfillment-data ---> ob[Immutable Off-Chain\nOrderbook] + client -- 2. fulfillAdvancedOrder ---> seaport[ImmutableSeaport.sol] + seaport -- 3. authorizeOrder --> Zone + seaport -- 4a. transferFrom --> erc20[IERC20.sol] + seaport -- 4b. transferFrom --> erc721[IERC721.sol] + seaport -- 4c. safeTransferFrom --> erc1155[IERC1155.sol] + seaport -- 5. validateOrder --> Zone + subgraph Zone + direction TB + zone[ImmutableSignedZoneV3.sol] --> AccessControlEnumerable.sol + end +``` + +The sequence of events is as follows: + +1. The client makes a HTTP `POST .../fulfillment-data` request to the Immutable Orderbook, which will construct and sign an `extraData` payload to return to the client +2. The client calls `fulfillAdvancedOrder` or `fulfillAvailableAdvancedOrders` on `ImmutableSeaport.sol` to fulfill an order +3. `ImmutableSeaport.sol` executes the fufilment by transferring items between parties +4. `ImmutableSeaport.sol` calls `authorizeOrder` on `ImmutableSignedZoneV3.sol`, passing it the fulfilment execution details as well as the `extraData` parameter +5. `ImmutableSignedZoneV3.sol` authorizes the fulfilment execution details using the `extraData` payload, reverting if expectations are not met +6. `ImmutableSeaport.sol` calls `validateOrder` on `ImmutableSignedZoneV3.sol`, passing it the fulfilment execution details as well as the `extraData` parameter +7. `ImmutableSignedZoneV3.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met + +## Differences compared to ImmutableSignedZone (v2) + +The contract was developed based on ImmutableSignedZoneV2, with the addition of: + - Support for the Seaport 1.6 Zone interface `authorizeOrder` function + - SIP-7 Substandard 1 + - SIP-7 Substandard 7 +- SIP-7 Substandard 8 diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol new file mode 100644 index 00000000..d592d428 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol @@ -0,0 +1,53 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.20; + +import {AccessControl} from "openzeppelin-contracts-5.0.2/access/AccessControl.sol"; +import {IAccessControl} from "openzeppelin-contracts-5.0.2/access/IAccessControl.sol"; +import {AccessControlEnumerable} from "openzeppelin-contracts-5.0.2/access/extensions/AccessControlEnumerable.sol"; +import {ZoneAccessControlEventsAndErrors} from "./interfaces/ZoneAccessControlEventsAndErrors.sol"; + +/** + * @notice ZoneAccessControl encapsulates access control functionality for the zone. + */ +abstract contract ZoneAccessControl is AccessControlEnumerable, ZoneAccessControlEventsAndErrors { + /// @dev Zone manager manages the zone. + bytes32 public constant ZONE_MANAGER_ROLE = bytes32("ZONE_MANAGER"); + + /** + * @notice Constructor to setup initial default admin. + * + * @param owner The address to assign the DEFAULT_ADMIN_ROLE. + */ + constructor(address owner) { + // Grant admin role to the specified owner. + _grantRole(DEFAULT_ADMIN_ROLE, owner); + } + + /** + * @inheritdoc AccessControl + */ + function revokeRole( + bytes32 role, + address account + ) public override(AccessControl, IAccessControl) onlyRole(getRoleAdmin(role)) { + if (role == DEFAULT_ADMIN_ROLE && super.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 1) { + revert LastDefaultAdminRole(account); + } + + super.revokeRole(role, account); + } + + /** + * @inheritdoc AccessControl + */ + function renounceRole(bytes32 role, address callerConfirmation) public override(AccessControl, IAccessControl) { + if (role == DEFAULT_ADMIN_ROLE && super.getRoleMemberCount(DEFAULT_ADMIN_ROLE) == 1) { + revert LastDefaultAdminRole(callerConfirmation); + } + + super.renounceRole(role, callerConfirmation); + } +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5EventsAndErrors.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5EventsAndErrors.sol new file mode 100644 index 00000000..21bbf159 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5EventsAndErrors.sol @@ -0,0 +1,16 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +/** + * @notice SIP5EventsAndErrors contains errors and events + * related to zone interaction as specified in the SIP-5. + */ +interface SIP5EventsAndErrors { + /** + * @dev An event that is emitted when a SIP-5 compatible contract is deployed. + */ + event SeaportCompatibleContractDeployed(); +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5Interface.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5Interface.sol new file mode 100644 index 00000000..3f2af4a7 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5Interface.sol @@ -0,0 +1,25 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +import {Schema} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {SIP5EventsAndErrors} from "./SIP5EventsAndErrors.sol"; + +/** + * @dev SIP-5: Contract Metadata Interface for Seaport Contracts + * https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-5.md + */ +// This contract name re-use is OK because the SIP5Interface is an interface and not a deployable contract. +// slither-disable-next-line name-reused +interface SIP5Interface is SIP5EventsAndErrors { + /** + * @dev Returns Seaport metadata for this contract, returning the + * contract name and supported schemas. + * + * @return name The contract name + * @return schemas The supported SIPs + */ + function getSeaportMetadata() external view returns (string memory name, Schema[] memory schemas); +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6EventsAndErrors.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6EventsAndErrors.sol new file mode 100644 index 00000000..b9e517cf --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6EventsAndErrors.sol @@ -0,0 +1,18 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +/** + * @notice SIP6EventsAndErrors contains errors and events + * related to zone interaction as specified in the SIP-6. + */ +// This contract name re-use is OK because the SIP6EventsAndErrors is an interface and not a deployable contract. +// slither-disable-next-line name-reused +interface SIP6EventsAndErrors { + /** + * @dev Revert with an error if SIP-6 version byte is not supported. + */ + error UnsupportedExtraDataVersion(uint8 version); +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6Interface.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6Interface.sol new file mode 100644 index 00000000..3739500c --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6Interface.sol @@ -0,0 +1,14 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +import {SIP6EventsAndErrors} from "./SIP6EventsAndErrors.sol"; + +/** + * @dev SIP-6: Multi-Zone ExtraData + * https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-6.md + */ +// solhint-disable no-empty-blocks +interface SIP6Interface is SIP6EventsAndErrors {} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol new file mode 100644 index 00000000..4a6bade5 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -0,0 +1,134 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +/** + * @notice SIP7EventsAndErrors contains errors and events + * related to zone interaction as specified in the SIP-7. + */ +// This contract name re-use is OK because the SIP7EventsAndErrors is an interface and not a deployable contract. +// slither-disable-next-line name-reused +interface SIP7EventsAndErrors { + /** + * @dev Emit an event when a new signer is added. + */ + event SignerAdded(address signer); + + /** + * @dev Emit an event when a signer is removed. + */ + event SignerRemoved(address signer); + + /** + * @dev Revert with an error if trying to add a signer that is + * already active. + */ + error SignerAlreadyActive(address signer); + + /** + * @dev Revert with an error if trying to remove a signer that is + * not active. + */ + error SignerNotActive(address signer); + + /** + * @dev Revert with an error if a new signer is the zero address. + */ + error SignerCannotBeZeroAddress(); + + /** + * @dev Revert with an error if a removed signer is trying to be + * reauthorized. + */ + error SignerCannotBeReauthorized(address signer); + + /** + * @dev Revert with an error when the signature has expired. + */ + error SignatureExpired(uint256 currentTimestamp, uint256 expiration, bytes32 orderHash); + + /** + * @dev Revert with an error if the fulfiller does not match. + */ + error InvalidFulfiller(address expectedFulfiller, address actualFulfiller, bytes32 orderHash); + + /** + * @dev Revert with an error if supplied order extraData is invalid + * or improperly formatted. + */ + error InvalidExtraData(string reason, bytes32 orderHash); + + /** + * @dev Revert with an error if the Seaport address is the zero address. + * This is a custom error that is not part of the SIP-7 spec. + */ + error SeaportCannotBeZeroAddress(); + + /** + * @dev Revert with an error if the caller is not the Seaport contract. + * This is a custom error that is not part of the SIP-7 spec. + */ + error CallerNotSeaport(); + + /** + * @dev Revert with an error if no spent items are provided. + * This is a custom error that is not part of the SIP-7 spec. + */ + error NoSpentItems(bytes32 orderHash); + + /** + * @dev Revert with an error if no received items are provided. + * This is a custom error that is not part of the SIP-7 spec. + */ + error NoReceivedItems(bytes32 orderHash); + + /** + * @dev Revert with an error if substandard 1 validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard1Violation(bytes32 orderHash, uint256 actualIdentifier, uint256 expectedIdentifier); + + /** + * @dev Revert with an error if substandard 3 validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard3Violation(bytes32 orderHash); + + /** + * @dev Revert with an error if substandard 4 validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard4Violation(bytes32[] actualOrderHashes, bytes32[] expectedOrderHashes, bytes32 orderHash); + + /** + * @dev Revert with an error if substandard 6 validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard6Violation(uint256 actualSpentItemAmount, uint256 originalSpentItemAmount, bytes32 orderHash); + + /** + * @dev Revert with an error if substandard 7 identifier validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard7IdentifierViolation(bytes32 orderHash, uint256 actualIdentifier, uint256 expectedIdentifier); + + /** + * @dev Revert with an error if substandard 7 item type expectations are not met. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard7UnexpectedItemTypeViolation(bytes32 orderHash); + + /** + * @dev Revert with an error if substandard 8 identifier validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard8IdentifierViolation(bytes32 orderHash, uint256 actualIdentifier, uint256 expectedIdentifier); + + /** + * @dev Revert with an error if substandard 8 item type expectations are not met. + * This is a custom error that is not part of the SIP-7 spec. + */ + error Substandard8UnexpectedItemTypeViolation(bytes32 orderHash); +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7Interface.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7Interface.sol new file mode 100644 index 00000000..a8d4d1e7 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7Interface.sol @@ -0,0 +1,74 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +import {SIP7EventsAndErrors} from "./SIP7EventsAndErrors.sol"; + +/** + * @title SIP7Interface + * @author ryanio, Immutable + * @notice ImmutableSignedZone is an implementation of SIP-7 that requires orders + * to be signed by an approved signer. + * https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md + * + */ +// This contract name re-use is OK because the SIP7Interface is an interface and not a deployable contract. +// slither-disable-next-line name-reused +interface SIP7Interface is SIP7EventsAndErrors { + /** + * @dev The struct for storing signer info. + */ + struct SignerInfo { + /// @dev If the signer is currently active. + bool active; + /// @dev If the signer has been active before. + bool previouslyActive; + } + + /** + * @notice Add a new signer to the zone. + * + * @param signer The new signer address to add. + */ + function addSigner(address signer) external; + + /** + * @notice Remove an active signer from the zone. + * + * @param signer The signer address to remove. + */ + function removeSigner(address signer) external; + + /** + * @notice Update the API endpoint returned by this zone. + * + * @param newApiEndpoint The new API endpoint. + */ + function updateAPIEndpoint(string calldata newApiEndpoint) external; + + /** + * @notice Update the documentation URI returned by this zone. + * + * @param newDocumentationURI The new documentation URI. + */ + function updateDocumentationURI(string calldata newDocumentationURI) external; + + /** + * @notice Returns signing information about the zone. + * + * @return domainSeparator The domain separator used for signing. + * @return apiEndpoint The API endpoint to get signatures for orders + * using this zone. + */ + function sip7Information() + external + view + returns ( + bytes32 domainSeparator, + string memory apiEndpoint, + uint256[] memory substandards, + string memory documentationURI + ); +} diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/ZoneAccessControlEventsAndErrors.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/ZoneAccessControlEventsAndErrors.sol new file mode 100644 index 00000000..cf86ab3b --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/ZoneAccessControlEventsAndErrors.sol @@ -0,0 +1,16 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable compiler-version +pragma solidity ^0.8.17; + +/** + * @notice ZoneAccessControlEventsAndErrors contains errors and events + * related to zone access control. + */ +interface ZoneAccessControlEventsAndErrors { + /** + * @dev Revert with an error if revoking last DEFAULT_ADMIN_ROLE. + */ + error LastDefaultAdminRole(address account); +} diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 00000000..7623dfb9 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,71 @@ +{ + "lib/axelar-gmp-sdk-solidity": { + "tag": { + "name": "v5.8.0", + "rev": "3f6ae1a1d22590e1c9b6af66781adc72148ee447" + } + }, + "lib/creator-token-standards": { + "tag": { + "name": "v5.0.0", + "rev": "980a63b33591d568b6e04b45f37deba05a55f787" + } + }, + "lib/forge-std": { + "rev": "1d9650e951204a0ddce9ff89c32f1997984cef4d" + }, + "lib/immutable-seaport-1.5.0+im1.3": { + "tag": { + "name": "1.5.0+im.1.3", + "rev": "ae061dc008105dd8d05937df9ad9a676f878cbf9" + } + }, + "lib/immutable-seaport-1.6.0+im4": { + "tag": { + "name": "1.6.0+im4", + "rev": "e058101dbe69b403352598ed989c3afd845e9793" + } + }, + "lib/immutable-seaport-core-1.5.0+im1": { + "tag": { + "name": "1.5.0+im.1", + "rev": "33e9030f308500b422926a1be12d7a1e4d6adc06" + } + }, + "lib/immutable-seaport-core-1.6.0+im2": { + "tag": { + "name": "1.6.0+im2", + "rev": "9ad91d82609e937a9ba3ef330df396b05f384e44" + } + }, + "lib/openzeppelin-contracts-4.9.3": { + "tag": { + "name": "v4.9.3", + "rev": "fd81a96f01cc42ef1c9a5399364968d0e07e9e90" + } + }, + "lib/openzeppelin-contracts-5.0.2": { + "tag": { + "name": "v5.0.2", + "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" + } + }, + "lib/openzeppelin-contracts-upgradeable-4.9.3": { + "tag": { + "name": "v4.9.3", + "rev": "3d4c0d5741b131c231e558d7a6213392ab3672a5" + } + }, + "lib/solidity-bits": { + "tag": { + "name": "v0.4.0", + "rev": "c243a888782b61542da380ac92e218c676427b50" + } + }, + "lib/solidity-bytes-utils": { + "tag": { + "name": "v0.8.0", + "rev": "6458fb2780a3092bc756e737f246be1de6d3d362" + } + } +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 2dc610a3..b0c1e0db 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,5 +1,6 @@ import * as dotenv from "dotenv"; +import "@nomicfoundation/hardhat-foundry"; import { HardhatUserConfig, task } from "hardhat/config"; import "@nomiclabs/hardhat-ethers"; import "@nomiclabs/hardhat-etherscan"; @@ -26,6 +27,16 @@ task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { const config: HardhatUserConfig = { solidity: { compilers: [ + { + version: "0.8.24", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + evmVersion: "cancun", + }, + }, { version: "0.8.19", settings: { @@ -91,12 +102,26 @@ const config: HardhatUserConfig = { }, }, }, + "contracts/trading/seaport16/ImmutableSeaport.sol": { + version: "0.8.24", + settings: { + viaIR: true, + optimizer: { + enabled: true, + runs: 10, + }, + evmVersion: "cancun", + }, + }, }, }, paths: { tests: "./test", }, networks: { + hardhat: { + hardfork: "cancun", + }, sepolia: { url: process.env.SEPOLIA_URL || "", accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], diff --git a/lib/creator-token-standards b/lib/creator-token-standards new file mode 160000 index 00000000..980a63b3 --- /dev/null +++ b/lib/creator-token-standards @@ -0,0 +1 @@ +Subproject commit 980a63b33591d568b6e04b45f37deba05a55f787 diff --git a/lib/immutable-seaport-1.6.0+im4 b/lib/immutable-seaport-1.6.0+im4 new file mode 160000 index 00000000..e058101d --- /dev/null +++ b/lib/immutable-seaport-1.6.0+im4 @@ -0,0 +1 @@ +Subproject commit e058101dbe69b403352598ed989c3afd845e9793 diff --git a/lib/immutable-seaport-core-1.6.0+im2 b/lib/immutable-seaport-core-1.6.0+im2 new file mode 160000 index 00000000..9ad91d82 --- /dev/null +++ b/lib/immutable-seaport-core-1.6.0+im2 @@ -0,0 +1 @@ +Subproject commit 9ad91d82609e937a9ba3ef330df396b05f384e44 diff --git a/package.json b/package.json index 85382492..090e6a65 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "author": "Immutable", "license": "Apache-2.0", "devDependencies": { + "@nomicfoundation/hardhat-foundry": "^1.2.0", "@nomiclabs/hardhat-ethers": "^2.2.2", "@nomiclabs/hardhat-etherscan": "^3.1.6", "@nomiclabs/hardhat-waffle": "^2.0.5", @@ -55,7 +56,7 @@ "eslint-plugin-promise": "^6.1.1", "ethereum-waffle": "^4.0.10", "ethers": "^5.7.2", - "hardhat": "^2.12.7", + "hardhat": "^2.26.5", "hardhat-gas-reporter": "^1.0.9", "prettier": "^3.2.4", "prettier-plugin-solidity": "^1.3.1", @@ -70,6 +71,7 @@ }, "dependencies": { "@axelar-network/axelar-gmp-sdk-solidity": "^5.8.0", + "@limitbreak/creator-token-standards": "^5.0.0", "@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts-upgradeable": "^4.9.3", "@rari-capital/solmate": "^6.4.0", @@ -78,6 +80,9 @@ "openzeppelin-contracts-5.0.2": "npm:@openzeppelin/contracts@^5.0.2", "openzeppelin-contracts-upgradeable-4.9.3": "npm:@openzeppelin/contracts-upgradeable@^4.9.3", "seaport": "https://github.com/immutable/seaport.git#1.5.0+im.1.3", + "seaport-16": "https://github.com/immutable/seaport.git#1.6.0+im4", + "seaport-core-16": "https://github.com/immutable/seaport-core.git#1.6.0+im2", + "seaport-types-16": "npm:seaport-types@1.6.3", "solidity-bits": "^0.4.0", "solidity-bytes-utils": "^0.8.0" } diff --git a/remappings.txt b/remappings.txt index 5d8f7c53..b837a519 100644 --- a/remappings.txt +++ b/remappings.txt @@ -7,4 +7,9 @@ solidity-bytes-utils/=lib/solidity-bytes-utils/ seaport/contracts/=lib/immutable-seaport-1.5.0+im1.3/contracts/ seaport-core/=lib/immutable-seaport-core-1.5.0+im1/ seaport-types/=lib/immutable-seaport-1.5.0+im1.3/lib/seaport-types/ -@axelar-network/axelar-gmp-sdk-solidity=lib/axelar-gmp-sdk-solidity \ No newline at end of file +@axelar-network/axelar-gmp-sdk-solidity=lib/axelar-gmp-sdk-solidity +seaport-16/contracts/=lib/immutable-seaport-1.6.0+im4/contracts/ +seaport-core-16/=lib/immutable-seaport-core-1.6.0+im2/ +seaport-sol-16/=lib/immutable-seaport-1.6.0+im4/lib/seaport-sol/ +seaport-types-16/=lib/immutable-seaport-1.6.0+im4/lib/seaport-types/ +@limitbreak/creator-token-standards/=lib/creator-token-standards/ diff --git a/script/staking/StakeHolderScriptWIMX.t.sol b/script/staking/StakeHolderScriptWIMX.t.sol index 041cf14c..56d2fdca 100644 --- a/script/staking/StakeHolderScriptWIMX.t.sol +++ b/script/staking/StakeHolderScriptWIMX.t.sol @@ -97,7 +97,7 @@ contract StakeHolderScriptWIMX is Test { ComplexStakeHolderContractArgs memory stakeHolderArgs = ComplexStakeHolderContractArgs({distributeAdmin: distributeAdmin, token: token}); - ComplexTimelockContractArgs memory timelockArgs = + ComplexTimelockContractArgs memory timelockArgs = ComplexTimelockContractArgs({timeDelayInSeconds: timeDelayInSeconds, proposerAdmin: proposerAdmin, executorAdmin: executorAdmin}); _deployComplex(deploymentArgs, stakeHolderArgs, timelockArgs); } @@ -116,7 +116,7 @@ contract StakeHolderScriptWIMX is Test { SimpleStakeHolderContractArgs memory stakeHolderArgs = SimpleStakeHolderContractArgs({ - roleAdmin: roleAdmin, upgradeAdmin: upgradeAdmin, + roleAdmin: roleAdmin, upgradeAdmin: upgradeAdmin, distributeAdmin: distributeAdmin, token: token}); _deploySimple(deploymentArgs, stakeHolderArgs); } @@ -149,7 +149,7 @@ contract StakeHolderScriptWIMX is Test { * Deploy StakeHolderWIMXV2 using Create3, with the TimelockController. */ function _deployComplex( - ComplexDeploymentArgs memory deploymentArgs, + ComplexDeploymentArgs memory deploymentArgs, ComplexStakeHolderContractArgs memory stakeHolderArgs, ComplexTimelockContractArgs memory timelockArgs) private @@ -171,7 +171,7 @@ contract StakeHolderScriptWIMX is Test { executors[0] = timelockArgs.executorAdmin; // Create deployment bytecode and encode constructor args deploymentBytecode = abi.encodePacked( - type(TimelockController).creationCode, + type(TimelockController).creationCode, abi.encode( timelockArgs.timeDelayInSeconds, proposers, @@ -199,7 +199,7 @@ contract StakeHolderScriptWIMX is Test { // Deploy ERC1967Proxy via the Ownable Create3 factory. // Create init data for the ERC1967 Proxy bytes memory initData = abi.encodeWithSelector( - StakeHolderWIMXV2.initialize.selector, + StakeHolderWIMXV2.initialize.selector, timelockAddress, // roleAdmin timelockAddress, // upgradeAdmin stakeHolderArgs.distributeAdmin, @@ -223,13 +223,13 @@ contract StakeHolderScriptWIMX is Test { * Deploy StakeHolderWIMXV2 using an EOA and no time lock. */ function _deploySimple( - SimpleDeploymentArgs memory deploymentArgs, + SimpleDeploymentArgs memory deploymentArgs, SimpleStakeHolderContractArgs memory stakeHolderArgs) private returns (StakeHolderWIMXV2 stakeHolderContract) { bytes memory initData = abi.encodeWithSelector( - StakeHolderWIMXV2.initialize.selector, + StakeHolderWIMXV2.initialize.selector, stakeHolderArgs.roleAdmin, stakeHolderArgs.upgradeAdmin, stakeHolderArgs.distributeAdmin, @@ -281,7 +281,7 @@ contract StakeHolderScriptWIMX is Test { }); address distributeAdmin = makeAddr("distribute"); - ComplexStakeHolderContractArgs memory stakeHolderArgs = + ComplexStakeHolderContractArgs memory stakeHolderArgs = ComplexStakeHolderContractArgs({ distributeAdmin: distributeAdmin, token: address(erc20) @@ -291,7 +291,7 @@ contract StakeHolderScriptWIMX is Test { address proposer = makeAddr("proposer"); address executor = makeAddr("executor"); - ComplexTimelockContractArgs memory timelockArgs = + ComplexTimelockContractArgs memory timelockArgs = ComplexTimelockContractArgs({ timeDelayInSeconds: delay, proposerAdmin: proposer, @@ -301,10 +301,10 @@ contract StakeHolderScriptWIMX is Test { // Run deployment against forked testnet StakeHolderWIMXV2 stakeHolder; TimelockController timelockController; - (stakeHolder, timelockController) = + (stakeHolder, timelockController) = _deployComplex(deploymentArgs, stakeHolderArgs, timelockArgs); - _commonTest(true, IStakeHolder(stakeHolder), address(timelockController), + _commonTest(true, IStakeHolder(stakeHolder), address(timelockController), immTestNetCreate3, address(0), address(0), distributeAdmin); assertTrue(timelockController.hasRole(timelockController.PROPOSER_ROLE(), proposer), "Proposer not set correcrly"); @@ -331,7 +331,7 @@ contract StakeHolderScriptWIMX is Test { address upgradeAdmin = makeAddr("upgrade"); address distributeAdmin = makeAddr("distribute"); - SimpleStakeHolderContractArgs memory stakeHolderContractArgs = + SimpleStakeHolderContractArgs memory stakeHolderContractArgs = SimpleStakeHolderContractArgs({ roleAdmin: roleAdmin, upgradeAdmin: upgradeAdmin, @@ -342,13 +342,13 @@ contract StakeHolderScriptWIMX is Test { // Run deployment against forked testnet StakeHolderWIMXV2 stakeHolder = _deploySimple(deploymentArgs, stakeHolderContractArgs); - _commonTest(false, IStakeHolder(stakeHolder), address(0), + _commonTest(false, IStakeHolder(stakeHolder), address(0), deployer, roleAdmin, upgradeAdmin, distributeAdmin); } function _commonTest( - bool _isComplex, - IStakeHolder _stakeHolder, + bool _isComplex, + IStakeHolder _stakeHolder, address _timelockControl, address _deployer, address _roleAdmin, diff --git a/test/trading/seaport/ImmutableSeaportOperational.t.sol b/test/trading/seaport/ImmutableSeaportOperational.t.sol index f0a4b8f4..476b3d5c 100644 --- a/test/trading/seaport/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport/ImmutableSeaportOperational.t.sol @@ -85,7 +85,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS _checkFulfill(OrderType.PARTIAL_RESTRICTED); } - + function testRejectUnsupportedZones() public { // Create order with random zone address randomZone = makeAddr("randomZone"); @@ -119,7 +119,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS uint256 wrongSigner = 1; AdvancedOrder memory order = _prepareCheckFulfill(wrongSigner); - // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the + // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the // information going in is wrong, then the wrong signer will be derived. address derivedBadSigner = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; @@ -131,7 +131,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS function testRejectInvalidExtraData() public { AdvancedOrder memory order = _prepareCheckFulfillWithBadExtraData(); - // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the + // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the // information going in is wrong, then the wrong signer will be derived. address derivedBadSigner = 0xcE810B9B83082C93574784f403727369c3FE6955; @@ -180,7 +180,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS } - function _prepareCheckFulfill(OrderType _orderType, address _zone, uint256 _signer, bool _useBaseExtraData) internal returns (AdvancedOrder memory) { + function _prepareCheckFulfill(OrderType _orderType, address _zone, uint256 _signer, bool _useBadExtraData) internal returns (AdvancedOrder memory) { // Deploy test ERC721 erc721 = new TestERC721(); erc721.mint(address(sellerWallet), nftId); @@ -218,7 +218,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS bytes32 orderHash = immutableSeaport.getOrderHash(orderComponents); bytes memory extraData = _generateSip7Signature(orderHash, buyer, _signer, expiration, orderParams.consideration); - if (_useBaseExtraData) { + if (_useBadExtraData) { orderParams.consideration[0].recipient = payable(buyer); extraData = _generateSip7Signature(orderHash, buyer, _signer, expiration, orderParams.consideration); } @@ -227,4 +227,4 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS AdvancedOrder memory order = AdvancedOrder(orderParams, 1, 1, signature, extraData); return order; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.t.sol index ffc4da87..c601f465 100644 --- a/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.t.sol +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.t.sol @@ -683,30 +683,39 @@ contract ImmutableSignedZoneV2Test is /* _domainSeparator */ function test_domainSeparator_returnsCachedDomainSeparatorWhenChainIDMatchesValueSetOnDeployment() public { + address deployer = makeAddr("deployer"); + vm.startPrank(deployer); ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); + vm.stopPrank(); bytes32 domainSeparator = zone.exposed_domainSeparator(); - assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + assertEq(domainSeparator, bytes32(0xbad25e7f17ff3b4061bed225ecd03e8abd71c703fdbde22f16c1f74fd735b6a2)); } function test_domainSeparator_returnsUpdatedDomainSeparatorIfChainIDIsDifferentFromValueSetOnDeployment() public { + address deployer = makeAddr("deployer"); + vm.startPrank(deployer); ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); + vm.stopPrank(); bytes32 domainSeparatorCached = zone.exposed_domainSeparator(); vm.chainId(31338); bytes32 domainSeparatorDerived = zone.exposed_domainSeparator(); assertNotEq(domainSeparatorCached, domainSeparatorDerived); - assertEq(domainSeparatorDerived, bytes32(0x835aabb0d2af048df195a75a990b42533471d4a4e82842cd54a892eaac463d74)); + assertEq(domainSeparatorDerived, bytes32(0x0740bf4283e41ef4c00c821487ac2a857a60072a5eec82e44b700a0b13f52c2a)); } /* _deriveDomainSeparator */ function test_deriveDomainSeparator_returnsDomainSeparatorForChainID() public { + address deployer = makeAddr("deployer"); + vm.startPrank(deployer); ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); + vm.stopPrank(); bytes32 domainSeparator = zone.exposed_deriveDomainSeparator(); - assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + assertEq(domainSeparator, bytes32(0xbad25e7f17ff3b4061bed225ecd03e8abd71c703fdbde22f16c1f74fd735b6a2)); } /* _getSupportedSubstandards */ @@ -733,6 +742,7 @@ contract ImmutableSignedZoneV2Test is } /* _validateSubstandards */ + function test_validateSubstandards_revertsIfEmptyContext() public { ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); diff --git a/test/trading/seaport16/ImmutableSeaportBase.t.sol b/test/trading/seaport16/ImmutableSeaportBase.t.sol new file mode 100644 index 00000000..d47e394f --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportBase.t.sol @@ -0,0 +1,79 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2025 +// SPDX-License-Identifier: Apache-2 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ImmutableSeaport} from "../../../contracts/trading/seaport16/ImmutableSeaport.sol"; +import {ImmutableSignedZoneV3} from "../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol"; +import {SIP7EventsAndErrors} from "../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol"; + +import {ConduitController} from "seaport-core-16/src/conduit/ConduitController.sol"; +import {Conduit} from "seaport-core-16/src/conduit/Conduit.sol"; +import {Consideration} from "seaport-core-16/src/lib/Consideration.sol"; +import {OrderParameters, OrderComponents, Order, AdvancedOrder, FulfillmentComponent, FulfillmentComponent, CriteriaResolver} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType} from "seaport-types-16/src/lib/ConsiderationEnums.sol"; +import {ReceivedItem, SpentItem} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + + + + + + +abstract contract ImmutableSeaportBaseTest is Test { + event AllowedZoneSet(address zoneAddress, bool allowed); + + ImmutableSeaport public immutableSeaport; + ImmutableSignedZoneV3 public immutableSignedZone; + ConduitController public conduitController; + Conduit public conduit; + bytes32 public conduitKey; + address public conduitAddress; + address public owner; + address public zoneManager; + address public immutableSigner; + uint256 public immutableSignerPkey; + address public buyer; + address public seller; + uint256 public buyerPkey; + uint256 public sellerPkey; + + function setUp() public virtual { + // Set up chain ID + //uint256 chainId = block.chainid; + + // Create test addresses + owner = makeAddr("owner"); + zoneManager = makeAddr("zoneManager"); + (immutableSigner, immutableSignerPkey) = makeAddrAndKey("immutableSigner"); + (buyer, buyerPkey) = makeAddrAndKey("buyer"); + (seller, sellerPkey) = makeAddrAndKey("seller"); + + // Deploy contracts + + // The conduit key used to deploy the conduit. Note that the first twenty bytes of the conduit key must match the caller of this contract. + conduitKey = bytes32(uint256(uint160(owner)) << (256-160)); + conduitController = new ConduitController(); + vm.prank(owner); + conduitController.createConduit(conduitKey, owner); + bool exists; + (conduitAddress, exists) = conduitController.getConduit(conduitKey); + assertTrue(exists, "Condiut contract does not exist"); + conduit = Conduit(conduitAddress); + + immutableSeaport = new ImmutableSeaport(address(conduitController), owner); + + immutableSignedZone = new ImmutableSignedZoneV3("ImmutableSignedZone", address(immutableSeaport), "", "", owner); + bytes32 zoneManagerRole = immutableSignedZone.ZONE_MANAGER_ROLE(); + vm.prank(owner); + immutableSignedZone.grantRole(zoneManagerRole, zoneManager); + vm.prank(zoneManager); + immutableSignedZone.addSigner(immutableSigner); + + vm.prank(owner); + immutableSeaport.setAllowedZone(address(immutableSignedZone), true); + vm.prank(owner); + conduitController.updateChannel(conduitAddress, address(immutableSeaport), true); + } +} \ No newline at end of file diff --git a/test/trading/seaport16/ImmutableSeaportConfig.t.sol b/test/trading/seaport16/ImmutableSeaportConfig.t.sol new file mode 100644 index 00000000..1d85ac14 --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportConfig.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {ImmutableSeaportBaseTest} from "./ImmutableSeaportBase.t.sol"; + +contract ImmutableSeaportConfigTest is ImmutableSeaportBaseTest { + + function testEmitsAllowedZoneSetEvent() public { + address zone = makeAddr("zone"); + bool allowed = true; + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit AllowedZoneSet(zone, allowed); + immutableSeaport.setAllowedZone(zone, allowed); + } +} diff --git a/test/trading/seaport16/ImmutableSeaportHarness.t.sol b/test/trading/seaport16/ImmutableSeaportHarness.t.sol new file mode 100644 index 00000000..e74ea8c0 --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportHarness.t.sol @@ -0,0 +1,27 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ImmutableSeaport} from "../../../contracts/trading/seaport16/ImmutableSeaport.sol"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSeaportHarness is ImmutableSeaport { + constructor(address conduitController, address owner) ImmutableSeaport(conduitController, owner) {} + + function exposed_domainSeparator() external view returns (bytes32) { + return _domainSeparator(); + } + + function exposed_deriveEIP712Digest(bytes32 domainSeparator, bytes32 orderHash) + external + pure + returns (bytes32 value) + { + return _deriveEIP712Digest(domainSeparator, orderHash); + } +} + +// solhint-enable func-name-mixedcase diff --git a/test/trading/seaport16/ImmutableSeaportOperational.t.sol b/test/trading/seaport16/ImmutableSeaportOperational.t.sol new file mode 100644 index 00000000..afe94ebc --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportOperational.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {ImmutableSeaportBaseTest} from "./ImmutableSeaportBase.t.sol"; + + +import "forge-std/Test.sol"; +import {ImmutableSeaportTestHelper} from "./ImmutableSeaportTestHelper.t.sol"; +import {ImmutableSeaport} from "../../../contracts/trading/seaport16/ImmutableSeaport.sol"; +import {SIP7EventsAndErrors} from "../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol"; + + +import {ConduitController} from "seaport-core-16/src/conduit/ConduitController.sol"; +import {Conduit} from "seaport-core-16/src/conduit/Conduit.sol"; +import {Consideration} from "seaport-core-16/src/lib/Consideration.sol"; +import {OrderParameters, OrderComponents, Order, AdvancedOrder, FulfillmentComponent, FulfillmentComponent, CriteriaResolver} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType} from "seaport-types-16/src/lib/ConsiderationEnums.sol"; +import {ConsiderationItem, OfferItem, ReceivedItem, SpentItem} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + + + + +contract TestERC721 is ERC721("Test721", "TST721") { + function mint(address to, uint256 tokenId) public returns (bool) { + _mint(to, tokenId); + return true; + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } +} + +// A wallet rather than an EOA needs to be used for the seller because code in forge detects +// the seller as a contract when created it is created with makeAddr. +contract SellerWallet { + bytes4 private constant SELECTOR_ERC1271_BYTES_BYTES = 0x20c13b0b; + bytes4 private constant SELECTOR_ERC1271_BYTES32_BYTES = 0x1626ba7e; + + function isValidSignature(bytes calldata /*_data */, bytes calldata /*_signatures*/) external pure returns (bytes4) { +// if (_signatureValidationInternal(_subDigest(keccak256(_data)), _signatures)) { + return SELECTOR_ERC1271_BYTES_BYTES; + // } + // return 0; + } + + function isValidSignature(bytes32 /*_hash*/, bytes calldata /*_signatures*/) external pure returns (bytes4) { + // if (_signatureValidationInternal(_subDigest(_hash), _signatures)) { + return SELECTOR_ERC1271_BYTES32_BYTES; + // } + // return 0; + } + + function setApprovalForAll(address _erc721, address _seaport) external { + ERC721(_erc721).setApprovalForAll(_seaport, true); + } + + receive() external payable { } +} + + + + +contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableSeaportTestHelper { + SellerWallet public sellerWallet; + TestERC721 public erc721; + uint256 public nftId; + + function setUp() public override { + super.setUp(); + _setFulfillerAndZone(buyer, address(immutableSignedZone)); + sellerWallet = new SellerWallet(); + nftId = 1; + vm.deal(buyer, 10 ether); + } + + + function testFulfillFullRestrictedOrder() public { + _checkFulfill(OrderType.FULL_RESTRICTED); + } + + function testFulfillPartialRestrictedOrder() public { + _checkFulfill(OrderType.PARTIAL_RESTRICTED); + } + + function testRejectUnsupportedZones() public { + // Create order with random zone + address randomZone = makeAddr("randomZone"); + AdvancedOrder memory order = _prepareCheckFulfill(randomZone); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.InvalidZone.selector, randomZone)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectFullOpenOrder() public { + AdvancedOrder memory order = _prepareCheckFulfill(OrderType.FULL_OPEN); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.OrderNotRestricted.selector)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectDisabledZone() public { + AdvancedOrder memory order = _prepareCheckFulfill(); + + vm.prank(owner); + immutableSeaport.setAllowedZone(address(immutableSignedZone), false); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.InvalidZone.selector, address(immutableSignedZone))); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectWrongSigner() public { + uint256 wrongSigner = 1; + AdvancedOrder memory order = _prepareCheckFulfill(wrongSigner); + + // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the + // information going in is wrong, then the wrong signer will be derived. + address derivedBadSigner = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + function testRejectInvalidExtraData() public { + AdvancedOrder memory order = _prepareCheckFulfillWithBadExtraData(); + + // The algorithm inside fulfillAdvancedOrder uses ecRecover to determine the signer. If the + // information going in is wrong, then the wrong signer will be derived. + address derivedBadSigner = 0xbB5E3df75ae272CcEb6CEB0F4A4BCfd3AfE3f27D; + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } + + + function _checkFulfill(OrderType _orderType) internal { + AdvancedOrder memory order = _prepareCheckFulfill(_orderType); + + // Record balances before + uint256 sellerBalanceBefore = address(sellerWallet).balance; + uint256 buyerBalanceBefore = address(buyer).balance; + + // Fulfill order + vm.prank(buyer); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + + // Verify results + assertEq(erc721.ownerOf(nftId), buyer, "Owner of NFT not buyer"); + assertEq(address(sellerWallet).balance, sellerBalanceBefore + 10 ether, "Seller incorrect final balance"); + assertEq(address(buyer).balance, buyerBalanceBefore - 10 ether, "Buyer incorrect final balance"); + } + + function _prepareCheckFulfill() internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, address(immutableSignedZone), immutableSignerPkey, false); + } + + function _prepareCheckFulfill(OrderType _orderType) internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(_orderType, address(immutableSignedZone), immutableSignerPkey, false); + } + + + function _prepareCheckFulfill(address _zone) internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, _zone, immutableSignerPkey, false); + } + + function _prepareCheckFulfill(uint256 _signer) internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, address(immutableSignedZone), _signer, false); + } + + function _prepareCheckFulfillWithBadExtraData() internal returns (AdvancedOrder memory) { + return _prepareCheckFulfill(OrderType.PARTIAL_RESTRICTED, address(immutableSignedZone), immutableSignerPkey, true); + } + + + function _prepareCheckFulfill(OrderType _orderType, address _zone, uint256 _signer, bool _useBadExtraData) internal returns (AdvancedOrder memory) { + // Deploy test ERC721 + erc721 = new TestERC721(); + erc721.mint(address(sellerWallet), nftId); + sellerWallet.setApprovalForAll(address(erc721), conduitAddress); + uint64 expiration = uint64(block.timestamp + 90); + + // Create order + OrderParameters memory orderParams = OrderParameters({ + offerer: address(sellerWallet), + zone: _zone, + offer: _createOfferItems(address(erc721), nftId), + consideration: _createConsiderationItems(address(sellerWallet), 10 ether), + orderType: _orderType, + startTime: 0, + endTime: expiration, + zoneHash: bytes32(0), + salt: 0, + conduitKey: conduitKey, + totalOriginalConsiderationItems: 1 + }); + + OrderComponents memory orderComponents = OrderComponents({ + offerer: orderParams.offerer, + zone: orderParams.zone, + offer: orderParams.offer, + consideration: orderParams.consideration, + orderType: orderParams.orderType, + startTime: orderParams.startTime, + endTime: orderParams.endTime, + zoneHash: orderParams.zoneHash, + salt: orderParams.salt, + conduitKey: orderParams.conduitKey, + counter: immutableSeaport.getCounter(orderParams.offerer) + }); + + bytes32 orderHash = immutableSeaport.getOrderHash(orderComponents); + bytes memory extraData = _generateSip7Signature(orderHash, buyer, _signer, expiration, orderParams.consideration); + if (_useBadExtraData) { + orderParams.consideration[0].recipient = payable(buyer); + extraData = _generateSip7Signature(orderHash, buyer, _signer, expiration, orderParams.consideration); + } + bytes memory signature = _signOrder(sellerPkey, orderHash); + + AdvancedOrder memory order = AdvancedOrder(orderParams, 1, 1, signature, extraData); + return order; + } +} \ No newline at end of file diff --git a/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol b/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol new file mode 100644 index 00000000..d2ab6fc1 --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol @@ -0,0 +1,903 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +// solhint-disable-next-line no-global-import +import "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ItemType, OrderType} from "seaport-types-16/src/lib/ConsiderationEnums.sol"; +import { + AdvancedOrder, + ConsiderationItem, + CriteriaResolver, + OrderComponents, + OfferItem, + OrderParameters, + ReceivedItem +} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {ConduitController} from "../../../contracts/trading/seaport16/conduit/ConduitController.sol"; +import {ImmutableSeaportHarness} from "./ImmutableSeaportHarness.t.sol"; +import {IImmutableERC1155} from "../seaport/utils/IImmutableERC1155.t.sol"; +import {IImmutableERC721} from "../seaport/utils/IImmutableERC721.t.sol"; +import {IOperatorAllowlistUpgradeable} from "../seaport/utils/IOperatorAllowlistUpgradeable.t.sol"; +import {SigningTestHelper} from "../seaport/utils/SigningTestHelper.t.sol"; +import {IImmutableSignedZoneV3Harness} from "./zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol"; + +// solhint-disable func-name-mixedcase, private-vars-leading-underscore + +contract ImmutableSeaportSignedZoneV3IntegrationTest is Test, SigningTestHelper { + // Foundry artifacts allow the test to deploy contracts separately that aren't compatible with + // the solidity version compiler that the test and its dependencies resolve to. + string private constant OPERATOR_ALLOWLIST_ARTIFACT = + "./foundry-out/OperatorAllowlistUpgradeable.sol/OperatorAllowlistUpgradeable.json"; + string private constant ERC1155_ARTIFACT = "./foundry-out/ImmutableERC1155.sol/ImmutableERC1155.json"; + string private constant ERC20_ARTIFACT = + "./foundry-out/ImmutableERC20FixedSupplyNoBurn.sol/ImmutableERC20FixedSupplyNoBurn.json"; + string private constant ERC721_ARTIFACT = "./foundry-out/ImmutableERC721.sol/ImmutableERC721.json"; + string private constant ZONE_ARTIFACT = + "./foundry-out/ImmutableSignedZoneV3Harness.t.sol/ImmutableSignedZoneV3Harness.json"; + + address private immutable OWNER = makeAddr("owner"); + address private immutable ZONE_MANAGER = makeAddr("zone_manager"); + address private immutable SIGNER; + uint256 private immutable SIGNER_PRIVATE_KEY; + address private immutable FULFILLER = makeAddr("fulfiller"); + address private immutable FULFILLER_TWO = makeAddr("fulfiller_two"); + address private immutable OFFERER; + uint256 private immutable OFFERER_PRIVATE_KEY; + address private immutable PROTOCOL_FEE_RECEIVER = makeAddr("protocol_fee_receiver"); + address private immutable ROYALTY_FEE_RECEIVER = makeAddr("royalty_fee_receiver"); + address private immutable ECOSYSTEM_FEE_RECEIVER = makeAddr("ecosystem_fee_receiver"); + + ImmutableSeaportHarness private seaport; + IImmutableSignedZoneV3Harness private zone; + IERC20 private erc20Token; + IImmutableERC1155 private erc1155Token; + IImmutableERC721 private erc721Token; + + constructor() { + (SIGNER, SIGNER_PRIVATE_KEY) = makeAddrAndKey("signer"); + (OFFERER, OFFERER_PRIVATE_KEY) = makeAddrAndKey("offerer"); + } + + function setUp() public { + // operator allowlist + IOperatorAllowlistUpgradeable operatorAllowlist = + IOperatorAllowlistUpgradeable(deployCode(OPERATOR_ALLOWLIST_ARTIFACT)); + operatorAllowlist.initialize(OWNER, OWNER, OWNER); + + // tokens + erc20Token = + IERC20(deployCode(ERC20_ARTIFACT, abi.encode("TestERC20", "ERC20", type(uint256).max, OWNER, OWNER))); + erc721Token = IImmutableERC721( + deployCode( + ERC721_ARTIFACT, + abi.encode( + OWNER, "TestERC721", "ERC721", "", "", address(operatorAllowlist), ROYALTY_FEE_RECEIVER, uint96(100) + ) + ) + ); + vm.prank(OWNER); + erc721Token.grantMinterRole(OWNER); + erc1155Token = IImmutableERC1155( + deployCode( + ERC1155_ARTIFACT, + abi.encode(OWNER, "TestERC1155", "", "", address(operatorAllowlist), ROYALTY_FEE_RECEIVER, uint96(100)) + ) + ); + vm.prank(OWNER); + erc1155Token.grantMinterRole(OWNER); + + // seaport + ConduitController conduitController = new ConduitController(); + seaport = new ImmutableSeaportHarness(address(conduitController), OWNER); + + // zone + zone = IImmutableSignedZoneV3Harness( + deployCode( + ZONE_ARTIFACT, + abi.encode("MyZoneName", address(seaport), "https://www.immutable.com", "https://www.immutable.com/docs", OWNER) + ) + ); + vm.prank(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, ZONE_MANAGER); + vm.prank(ZONE_MANAGER); + zone.addSigner(SIGNER); + + // set allowed zone + vm.prank(OWNER); + seaport.setAllowedZone(address(zone), true); + + // operator allowlist addresses + address[] memory allowlistAddress = new address[](1); + allowlistAddress[0] = address(seaport); + vm.prank(OWNER); + operatorAllowlist.addAddressesToAllowlist(allowlistAddress); + } + + function test_fulfillAdvancedOrder_withCompleteFulfilment() public { + // offer items + OfferItem[] memory offerItems = new OfferItem[](1); + offerItems[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(erc721Token), + identifierOrCriteria: uint256(50), + startAmount: uint256(1), + endAmount: uint256(1) + }); + + // consideration items + ConsiderationItem[] memory originalConsiderationItems = new ConsiderationItem[](1); + // original item + originalConsiderationItems[0] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(200_000_000_000_000_000_000), // 200^18 + endAmount: uint256(200_000_000_000_000_000_000), // 200^18 + recipient: payable(OFFERER) + }); + + ConsiderationItem[] memory considerationItems = new ConsiderationItem[](4); + considerationItems[0] = originalConsiderationItems[0]; + // protocol fee - 2% + considerationItems[1] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(4_000_000_000_000_000_000), + endAmount: uint256(4_000_000_000_000_000_000), + recipient: payable(PROTOCOL_FEE_RECEIVER) + }); + // royalty fee - 1% + considerationItems[2] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(2_000_000_000_000_000_000), + endAmount: uint256(2_000_000_000_000_000_000), + recipient: payable(ROYALTY_FEE_RECEIVER) + }); + // ecosystem fee - 3% + considerationItems[3] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(6_000_000_000_000_000_000), + endAmount: uint256(6_000_000_000_000_000_000), + recipient: payable(ECOSYSTEM_FEE_RECEIVER) + }); + + // order + OrderParameters memory orderParameters = OrderParameters({ + offerer: OFFERER, + zone: address(zone), + offer: offerItems, + consideration: considerationItems, + orderType: OrderType.FULL_RESTRICTED, + startTime: uint256(0), + endTime: uint256(5000), + zoneHash: bytes32(0), + salt: uint256(123), + conduitKey: bytes32(0), + totalOriginalConsiderationItems: uint256(1) + }); + + // order hash + bytes32 orderHash = seaport.getOrderHash( + OrderComponents({ + offerer: orderParameters.offerer, + zone: orderParameters.zone, + offer: orderParameters.offer, + consideration: originalConsiderationItems, + orderType: orderParameters.orderType, + startTime: orderParameters.startTime, + endTime: orderParameters.endTime, + zoneHash: orderParameters.zoneHash, + salt: orderParameters.salt, + conduitKey: orderParameters.conduitKey, + counter: seaport.getCounter(orderParameters.offerer) + }) + ); + + // order signature + bytes memory orderSignature; + { + bytes32 orderDigest = seaport.exposed_deriveEIP712Digest(seaport.exposed_domainSeparator(), orderHash); + orderSignature = _sign(OFFERER_PRIVATE_KEY, orderDigest); + } + + // extra data + bytes memory extraData; + { + ReceivedItem[] memory expectedReceivedItems = new ReceivedItem[](4); + expectedReceivedItems[0] = ReceivedItem({ + itemType: considerationItems[0].itemType, + token: considerationItems[0].token, + identifier: considerationItems[0].identifierOrCriteria, + amount: considerationItems[0].startAmount, + recipient: considerationItems[0].recipient + }); + expectedReceivedItems[1] = ReceivedItem({ + itemType: considerationItems[1].itemType, + token: considerationItems[1].token, + identifier: considerationItems[1].identifierOrCriteria, + amount: considerationItems[1].startAmount, + recipient: considerationItems[1].recipient + }); + expectedReceivedItems[2] = ReceivedItem({ + itemType: considerationItems[2].itemType, + token: considerationItems[2].token, + identifier: considerationItems[2].identifierOrCriteria, + amount: considerationItems[2].startAmount, + recipient: considerationItems[2].recipient + }); + expectedReceivedItems[3] = ReceivedItem({ + itemType: considerationItems[3].itemType, + token: considerationItems[3].token, + identifier: considerationItems[3].identifierOrCriteria, + amount: considerationItems[3].startAmount, + recipient: considerationItems[3].recipient + }); + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(expectedReceivedItems, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x06), offerItems[0].startAmount, substandard6Data); + bytes32 eip712SignedOrderHash = + zone.exposed_deriveSignedOrderHash(FULFILLER, uint64(4000), orderHash, context); + extraData = abi.encodePacked( + bytes1(0), + FULFILLER, + uint64(4000), + _signCompact( + SIGNER_PRIVATE_KEY, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash) + ), + context + ); + } + + // advanced order + AdvancedOrder memory advancedOrder = AdvancedOrder({ + parameters: orderParameters, + numerator: uint120(1), + denominator: uint120(1), + signature: orderSignature, + extraData: extraData + }); + + // mints + vm.prank(OWNER); + erc20Token.transfer( + FULFILLER, + ( + considerationItems[0].startAmount + considerationItems[1].startAmount + + considerationItems[2].startAmount + considerationItems[3].startAmount + ) + ); + vm.prank(OWNER); + erc721Token.safeMint(OFFERER, offerItems[0].identifierOrCriteria); + + // approvals + vm.prank(OFFERER); + erc721Token.setApprovalForAll(address(seaport), true); + vm.prank(FULFILLER); + erc20Token.approve(address(seaport), type(uint256).max); + + // fulfillment + vm.prank(FULFILLER); + seaport.fulfillAdvancedOrder(advancedOrder, new CriteriaResolver[](0), bytes32(0), FULFILLER); + + // assertions + assertEq(erc721Token.balanceOf(OFFERER), 0); + assertEq(erc721Token.balanceOf(FULFILLER), offerItems[0].startAmount); + assertEq(erc20Token.balanceOf(OFFERER), considerationItems[0].startAmount); + assertEq(erc20Token.balanceOf(FULFILLER), 0); + assertEq(erc20Token.balanceOf(PROTOCOL_FEE_RECEIVER), considerationItems[1].startAmount); + assertEq(erc20Token.balanceOf(ROYALTY_FEE_RECEIVER), considerationItems[2].startAmount); + assertEq(erc20Token.balanceOf(ECOSYSTEM_FEE_RECEIVER), considerationItems[3].startAmount); + } + + function test_fulfillAdvancedOrder_withPartialFill() public { + // offer items + OfferItem[] memory offerItems = new OfferItem[](1); + offerItems[0] = OfferItem({ + itemType: ItemType.ERC1155, + token: address(erc1155Token), + identifierOrCriteria: uint256(50), + startAmount: uint256(100), + endAmount: uint256(100) + }); + + // consideration items + ConsiderationItem[] memory originalConsiderationItems = new ConsiderationItem[](1); + // original item + originalConsiderationItems[0] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(200_000_000_000_000_000_000), // 200^18 + endAmount: uint256(200_000_000_000_000_000_000), // 200^18 + recipient: payable(OFFERER) + }); + + ConsiderationItem[] memory considerationItems = new ConsiderationItem[](4); + considerationItems[0] = originalConsiderationItems[0]; + // protocol fee - 2% + considerationItems[1] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(4_000_000_000_000_000_000), + endAmount: uint256(4_000_000_000_000_000_000), + recipient: payable(PROTOCOL_FEE_RECEIVER) + }); + // royalty fee - 1% + considerationItems[2] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(2_000_000_000_000_000_000), + endAmount: uint256(2_000_000_000_000_000_000), + recipient: payable(ROYALTY_FEE_RECEIVER) + }); + // ecosystem fee - 3% + considerationItems[3] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(6_000_000_000_000_000_000), + endAmount: uint256(6_000_000_000_000_000_000), + recipient: payable(ECOSYSTEM_FEE_RECEIVER) + }); + + // order + OrderParameters memory orderParameters = OrderParameters({ + offerer: OFFERER, + zone: address(zone), + offer: offerItems, + consideration: considerationItems, + orderType: OrderType.PARTIAL_RESTRICTED, + startTime: uint256(0), + endTime: uint256(5000), + zoneHash: bytes32(0), + salt: uint256(123), + conduitKey: bytes32(0), + totalOriginalConsiderationItems: uint256(1) + }); + + // order hash + bytes32 orderHash = seaport.getOrderHash( + OrderComponents({ + offerer: orderParameters.offerer, + zone: orderParameters.zone, + offer: orderParameters.offer, + consideration: originalConsiderationItems, + orderType: orderParameters.orderType, + startTime: orderParameters.startTime, + endTime: orderParameters.endTime, + zoneHash: orderParameters.zoneHash, + salt: orderParameters.salt, + conduitKey: orderParameters.conduitKey, + counter: seaport.getCounter(orderParameters.offerer) + }) + ); + + // order signature + bytes memory orderSignature; + { + bytes32 orderDigest = seaport.exposed_deriveEIP712Digest(seaport.exposed_domainSeparator(), orderHash); + orderSignature = _sign(OFFERER_PRIVATE_KEY, orderDigest); + } + + // extra data + bytes memory extraData; + { + ReceivedItem[] memory expectedReceivedItems = new ReceivedItem[](4); + expectedReceivedItems[0] = ReceivedItem({ + itemType: considerationItems[0].itemType, + token: considerationItems[0].token, + identifier: considerationItems[0].identifierOrCriteria, + amount: considerationItems[0].startAmount, + recipient: considerationItems[0].recipient + }); + expectedReceivedItems[1] = ReceivedItem({ + itemType: considerationItems[1].itemType, + token: considerationItems[1].token, + identifier: considerationItems[1].identifierOrCriteria, + amount: considerationItems[1].startAmount, + recipient: considerationItems[1].recipient + }); + expectedReceivedItems[2] = ReceivedItem({ + itemType: considerationItems[2].itemType, + token: considerationItems[2].token, + identifier: considerationItems[2].identifierOrCriteria, + amount: considerationItems[2].startAmount, + recipient: considerationItems[2].recipient + }); + expectedReceivedItems[3] = ReceivedItem({ + itemType: considerationItems[3].itemType, + token: considerationItems[3].token, + identifier: considerationItems[3].identifierOrCriteria, + amount: considerationItems[3].startAmount, + recipient: considerationItems[3].recipient + }); + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(expectedReceivedItems, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x06), offerItems[0].startAmount, substandard6Data); + bytes32 eip712SignedOrderHash = + zone.exposed_deriveSignedOrderHash(FULFILLER, uint64(4000), orderHash, context); + extraData = abi.encodePacked( + bytes1(0), + FULFILLER, + uint64(4000), + _signCompact( + SIGNER_PRIVATE_KEY, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash) + ), + context + ); + } + + // advanced order, fill 1/100th of the order + AdvancedOrder memory advancedOrder = AdvancedOrder({ + parameters: orderParameters, + numerator: uint120(1), + denominator: uint120(100), + signature: orderSignature, + extraData: extraData + }); + + // mints + vm.prank(OWNER); + erc20Token.transfer( + FULFILLER, + ( + considerationItems[0].startAmount + considerationItems[1].startAmount + + considerationItems[2].startAmount + considerationItems[3].startAmount + ) / 100 + ); + vm.prank(OWNER); + erc1155Token.safeMint(OFFERER, offerItems[0].identifierOrCriteria, offerItems[0].startAmount, new bytes(0)); + + // approvals + vm.prank(OFFERER); + erc1155Token.setApprovalForAll(address(seaport), true); + vm.prank(FULFILLER); + erc20Token.approve(address(seaport), type(uint256).max); + + // fulfillment + vm.prank(FULFILLER); + seaport.fulfillAdvancedOrder(advancedOrder, new CriteriaResolver[](0), bytes32(0), FULFILLER); + + // assertions + assertEq( + erc1155Token.balanceOf(OFFERER, offerItems[0].identifierOrCriteria), offerItems[0].startAmount * 99 / 100 + ); + assertEq( + erc1155Token.balanceOf(FULFILLER, offerItems[0].identifierOrCriteria), offerItems[0].startAmount * 1 / 100 + ); + assertEq(erc20Token.balanceOf(OFFERER), considerationItems[0].startAmount / 100); + assertEq(erc20Token.balanceOf(FULFILLER), 0); + assertEq(erc20Token.balanceOf(PROTOCOL_FEE_RECEIVER), considerationItems[1].startAmount / 100); + assertEq(erc20Token.balanceOf(ROYALTY_FEE_RECEIVER), considerationItems[2].startAmount / 100); + assertEq(erc20Token.balanceOf(ECOSYSTEM_FEE_RECEIVER), considerationItems[3].startAmount / 100); + } + + function test_fulfillAdvancedOrder_withMultiplePartialFills() public { + // offer items + OfferItem[] memory offerItems = new OfferItem[](1); + offerItems[0] = OfferItem({ + itemType: ItemType.ERC1155, + token: address(erc1155Token), + identifierOrCriteria: uint256(50), + startAmount: uint256(100), + endAmount: uint256(100) + }); + + // consideration items + ConsiderationItem[] memory originalConsiderationItems = new ConsiderationItem[](1); + // original item + originalConsiderationItems[0] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(200_000_000_000_000_000_000), // 200^18 + endAmount: uint256(200_000_000_000_000_000_000), // 200^18 + recipient: payable(OFFERER) + }); + + ConsiderationItem[] memory considerationItems = new ConsiderationItem[](4); + considerationItems[0] = originalConsiderationItems[0]; + // protocol fee - 2% + considerationItems[1] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(4_000_000_000_000_000_000), + endAmount: uint256(4_000_000_000_000_000_000), + recipient: payable(PROTOCOL_FEE_RECEIVER) + }); + // royalty fee - 1% + considerationItems[2] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(2_000_000_000_000_000_000), + endAmount: uint256(2_000_000_000_000_000_000), + recipient: payable(ROYALTY_FEE_RECEIVER) + }); + // ecosystem fee - 3% + considerationItems[3] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(6_000_000_000_000_000_000), + endAmount: uint256(6_000_000_000_000_000_000), + recipient: payable(ECOSYSTEM_FEE_RECEIVER) + }); + + // order + OrderParameters memory orderParameters = OrderParameters({ + offerer: OFFERER, + zone: address(zone), + offer: offerItems, + consideration: considerationItems, + orderType: OrderType.PARTIAL_RESTRICTED, + startTime: uint256(0), + endTime: uint256(5000), + zoneHash: bytes32(0), + salt: uint256(123), + conduitKey: bytes32(0), + totalOriginalConsiderationItems: uint256(1) + }); + + // order hash + bytes32 orderHash = seaport.getOrderHash( + OrderComponents({ + offerer: orderParameters.offerer, + zone: orderParameters.zone, + offer: orderParameters.offer, + consideration: originalConsiderationItems, + orderType: orderParameters.orderType, + startTime: orderParameters.startTime, + endTime: orderParameters.endTime, + zoneHash: orderParameters.zoneHash, + salt: orderParameters.salt, + conduitKey: orderParameters.conduitKey, + counter: seaport.getCounter(orderParameters.offerer) + }) + ); + + // order signature + bytes memory orderSignature; + { + bytes32 orderDigest = seaport.exposed_deriveEIP712Digest(seaport.exposed_domainSeparator(), orderHash); + orderSignature = _sign(OFFERER_PRIVATE_KEY, orderDigest); + } + + // extra data + bytes memory extraData; + { + ReceivedItem[] memory expectedReceivedItems = new ReceivedItem[](4); + expectedReceivedItems[0] = ReceivedItem({ + itemType: considerationItems[0].itemType, + token: considerationItems[0].token, + identifier: considerationItems[0].identifierOrCriteria, + amount: considerationItems[0].startAmount, + recipient: considerationItems[0].recipient + }); + expectedReceivedItems[1] = ReceivedItem({ + itemType: considerationItems[1].itemType, + token: considerationItems[1].token, + identifier: considerationItems[1].identifierOrCriteria, + amount: considerationItems[1].startAmount, + recipient: considerationItems[1].recipient + }); + expectedReceivedItems[2] = ReceivedItem({ + itemType: considerationItems[2].itemType, + token: considerationItems[2].token, + identifier: considerationItems[2].identifierOrCriteria, + amount: considerationItems[2].startAmount, + recipient: considerationItems[2].recipient + }); + expectedReceivedItems[3] = ReceivedItem({ + itemType: considerationItems[3].itemType, + token: considerationItems[3].token, + identifier: considerationItems[3].identifierOrCriteria, + amount: considerationItems[3].startAmount, + recipient: considerationItems[3].recipient + }); + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(expectedReceivedItems, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x06), offerItems[0].startAmount, substandard6Data); + bytes32 eip712SignedOrderHash = + zone.exposed_deriveSignedOrderHash(FULFILLER, uint64(4000), orderHash, context); + extraData = abi.encodePacked( + bytes1(0), + FULFILLER, + uint64(4000), + _signCompact( + SIGNER_PRIVATE_KEY, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash) + ), + context + ); + } + + // advanced order, fill 1/100th of the order + AdvancedOrder memory advancedOrder = AdvancedOrder({ + parameters: orderParameters, + numerator: uint120(1), + denominator: uint120(100), + signature: orderSignature, + extraData: extraData + }); + + // mints + vm.prank(OWNER); + erc20Token.transfer( + FULFILLER, + ( + considerationItems[0].startAmount + considerationItems[1].startAmount + + considerationItems[2].startAmount + considerationItems[3].startAmount + ) * 2 / 100 + ); + vm.prank(OWNER); + erc1155Token.safeMint(OFFERER, offerItems[0].identifierOrCriteria, offerItems[0].startAmount, new bytes(0)); + + // approvals + vm.prank(OFFERER); + erc1155Token.setApprovalForAll(address(seaport), true); + vm.prank(FULFILLER); + erc20Token.approve(address(seaport), type(uint256).max); + + // fulfill twice + vm.prank(FULFILLER); + seaport.fulfillAdvancedOrder(advancedOrder, new CriteriaResolver[](0), bytes32(0), FULFILLER); + vm.prank(FULFILLER); + seaport.fulfillAdvancedOrder(advancedOrder, new CriteriaResolver[](0), bytes32(0), FULFILLER); + + // assertions + assertEq( + erc1155Token.balanceOf(OFFERER, offerItems[0].identifierOrCriteria), offerItems[0].startAmount * 98 / 100 + ); + assertEq( + erc1155Token.balanceOf(FULFILLER, offerItems[0].identifierOrCriteria), offerItems[0].startAmount * 2 / 100 + ); + assertEq(erc20Token.balanceOf(OFFERER), considerationItems[0].startAmount * 2 / 100); + assertEq(erc20Token.balanceOf(FULFILLER), 0); + assertEq(erc20Token.balanceOf(PROTOCOL_FEE_RECEIVER), considerationItems[1].startAmount * 2 / 100); + assertEq(erc20Token.balanceOf(ROYALTY_FEE_RECEIVER), considerationItems[2].startAmount * 2 / 100); + assertEq(erc20Token.balanceOf(ECOSYSTEM_FEE_RECEIVER), considerationItems[3].startAmount * 2 / 100); + } + + function test_fulfillAdvancedOrder_withOverfilling() public { + // offer items + OfferItem[] memory offerItems = new OfferItem[](1); + offerItems[0] = OfferItem({ + itemType: ItemType.ERC1155, + token: address(erc1155Token), + identifierOrCriteria: uint256(50), + startAmount: uint256(100), + endAmount: uint256(100) + }); + + // consideration items + ConsiderationItem[] memory originalConsiderationItems = new ConsiderationItem[](1); + // original item + originalConsiderationItems[0] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(200_000_000_000_000_000_000), // 200^18 + endAmount: uint256(200_000_000_000_000_000_000), // 200^18 + recipient: payable(OFFERER) + }); + + ConsiderationItem[] memory considerationItems = new ConsiderationItem[](4); + considerationItems[0] = originalConsiderationItems[0]; + // protocol fee - 2% + considerationItems[1] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(4_000_000_000_000_000_000), + endAmount: uint256(4_000_000_000_000_000_000), + recipient: payable(PROTOCOL_FEE_RECEIVER) + }); + // royalty fee - 1% + considerationItems[2] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(2_000_000_000_000_000_000), + endAmount: uint256(2_000_000_000_000_000_000), + recipient: payable(ROYALTY_FEE_RECEIVER) + }); + // ecosystem fee - 3% + considerationItems[3] = ConsiderationItem({ + itemType: ItemType.ERC20, + token: address(erc20Token), + identifierOrCriteria: uint256(0), + startAmount: uint256(6_000_000_000_000_000_000), + endAmount: uint256(6_000_000_000_000_000_000), + recipient: payable(ECOSYSTEM_FEE_RECEIVER) + }); + + // order + OrderParameters memory orderParameters = OrderParameters({ + offerer: OFFERER, + zone: address(zone), + offer: offerItems, + consideration: considerationItems, + orderType: OrderType.PARTIAL_RESTRICTED, + startTime: uint256(0), + endTime: uint256(5000), + zoneHash: bytes32(0), + salt: uint256(123), + conduitKey: bytes32(0), + totalOriginalConsiderationItems: uint256(1) + }); + + // order hash + bytes32 orderHash = seaport.getOrderHash( + OrderComponents({ + offerer: orderParameters.offerer, + zone: orderParameters.zone, + offer: orderParameters.offer, + consideration: originalConsiderationItems, + orderType: orderParameters.orderType, + startTime: orderParameters.startTime, + endTime: orderParameters.endTime, + zoneHash: orderParameters.zoneHash, + salt: orderParameters.salt, + conduitKey: orderParameters.conduitKey, + counter: seaport.getCounter(orderParameters.offerer) + }) + ); + + // order signature + bytes memory orderSignature; + { + bytes32 orderDigest = seaport.exposed_deriveEIP712Digest(seaport.exposed_domainSeparator(), orderHash); + orderSignature = _sign(OFFERER_PRIVATE_KEY, orderDigest); + } + + // substandard 6 data expected received items + ReceivedItem[] memory expectedReceivedItems = new ReceivedItem[](4); + expectedReceivedItems[0] = ReceivedItem({ + itemType: considerationItems[0].itemType, + token: considerationItems[0].token, + identifier: considerationItems[0].identifierOrCriteria, + amount: considerationItems[0].startAmount, + recipient: considerationItems[0].recipient + }); + expectedReceivedItems[1] = ReceivedItem({ + itemType: considerationItems[1].itemType, + token: considerationItems[1].token, + identifier: considerationItems[1].identifierOrCriteria, + amount: considerationItems[1].startAmount, + recipient: considerationItems[1].recipient + }); + expectedReceivedItems[2] = ReceivedItem({ + itemType: considerationItems[2].itemType, + token: considerationItems[2].token, + identifier: considerationItems[2].identifierOrCriteria, + amount: considerationItems[2].startAmount, + recipient: considerationItems[2].recipient + }); + expectedReceivedItems[3] = ReceivedItem({ + itemType: considerationItems[3].itemType, + token: considerationItems[3].token, + identifier: considerationItems[3].identifierOrCriteria, + amount: considerationItems[3].startAmount, + recipient: considerationItems[3].recipient + }); + + // extra data + bytes memory extraData1; + bytes memory extraData2; + { + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(expectedReceivedItems, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x06), offerItems[0].startAmount, substandard6Data); + bytes32 eip712SignedOrderHash = + zone.exposed_deriveSignedOrderHash(FULFILLER, uint64(4000), orderHash, context); + extraData1 = abi.encodePacked( + bytes1(0), + FULFILLER, + uint64(4000), + _signCompact( + SIGNER_PRIVATE_KEY, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash) + ), + context + ); + } + { + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(expectedReceivedItems, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x06), offerItems[0].startAmount, substandard6Data); + bytes32 eip712SignedOrderHash = + zone.exposed_deriveSignedOrderHash(FULFILLER_TWO, uint64(4000), orderHash, context); + extraData2 = abi.encodePacked( + bytes1(0), + FULFILLER_TWO, + uint64(4000), + _signCompact( + SIGNER_PRIVATE_KEY, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash) + ), + context + ); + } + + // advanced order, fill 1/2 of the order + AdvancedOrder memory advancedOrder1 = AdvancedOrder({ + parameters: orderParameters, + numerator: uint120(50), + denominator: uint120(100), + signature: orderSignature, + extraData: extraData1 + }); + + // advanced order, attempt to fill the whole order + AdvancedOrder memory advancedOrder2 = AdvancedOrder({ + parameters: orderParameters, + numerator: uint120(1), + denominator: uint120(1), + signature: orderSignature, + extraData: extraData2 + }); + + // mints + vm.prank(OWNER); + erc20Token.transfer( + FULFILLER, + ( + considerationItems[0].startAmount + considerationItems[1].startAmount + + considerationItems[2].startAmount + considerationItems[3].startAmount + ) / 2 + ); + vm.prank(OWNER); + erc20Token.transfer( + FULFILLER_TWO, + ( + considerationItems[0].startAmount + considerationItems[1].startAmount + + considerationItems[2].startAmount + considerationItems[3].startAmount + ) + ); + vm.prank(OWNER); + erc1155Token.safeMint(OFFERER, offerItems[0].identifierOrCriteria, offerItems[0].startAmount, new bytes(0)); + + // approvals + vm.prank(OFFERER); + erc1155Token.setApprovalForAll(address(seaport), true); + vm.prank(FULFILLER); + erc20Token.approve(address(seaport), type(uint256).max); + vm.prank(FULFILLER_TWO); + erc20Token.approve(address(seaport), type(uint256).max); + + // fulfill twice + vm.prank(FULFILLER); + seaport.fulfillAdvancedOrder(advancedOrder1, new CriteriaResolver[](0), bytes32(0), FULFILLER); + vm.prank(FULFILLER_TWO); + seaport.fulfillAdvancedOrder(advancedOrder2, new CriteriaResolver[](0), bytes32(0), FULFILLER_TWO); + + // assertions + assertEq(erc1155Token.balanceOf(OFFERER, offerItems[0].identifierOrCriteria), 0); + assertEq(erc1155Token.balanceOf(FULFILLER, offerItems[0].identifierOrCriteria), offerItems[0].startAmount / 2); + assertEq( + erc1155Token.balanceOf(FULFILLER_TWO, offerItems[0].identifierOrCriteria), offerItems[0].startAmount / 2 + ); + assertEq(erc20Token.balanceOf(OFFERER), considerationItems[0].startAmount); + assertEq(erc20Token.balanceOf(FULFILLER), 0); + assertEq( + erc20Token.balanceOf(FULFILLER_TWO), + ( + considerationItems[0].startAmount + considerationItems[1].startAmount + + considerationItems[2].startAmount + considerationItems[3].startAmount + ) / 2 + ); + assertEq(erc20Token.balanceOf(PROTOCOL_FEE_RECEIVER), considerationItems[1].startAmount); + assertEq(erc20Token.balanceOf(ROYALTY_FEE_RECEIVER), considerationItems[2].startAmount); + assertEq(erc20Token.balanceOf(ECOSYSTEM_FEE_RECEIVER), considerationItems[3].startAmount); + } +} + +// solhint-enable func-name-mixedcase, private-vars-leading-underscore diff --git a/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol new file mode 100644 index 00000000..0a451f6c --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {SigningTestHelper} from "./utils/SigningTestHelper.t.sol"; +import {ItemType} from "seaport-types-16/src/lib/ConsiderationEnums.sol"; +import {ZoneParameters, ConsiderationItem, OfferItem, ReceivedItem, SpentItem} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {Math} from "openzeppelin-contracts-5.0.2/utils/math/Math.sol"; + + + +abstract contract ImmutableSeaportTestHelper is Test, SigningTestHelper { + string public constant ZONE_NAME = "ImmutableSignedZone"; + string public constant VERSION = "3.0"; + + address private theFulfiller; + + address private theZone; + + function _setFulfillerAndZone(address _fulfiller, address _zone) internal { + theFulfiller = _fulfiller; + theZone = _zone; + } + + // Helper functions + function _createZoneParameters(bytes memory _extraData) internal returns (ZoneParameters memory) { + bytes32 orderHash = keccak256("0x1234"); + return _createZoneParameters(_extraData, orderHash, _createMockConsideration(10)); + } + + function _createZoneParameters(bytes memory _extraData, bytes32 _orderHash) internal returns (ZoneParameters memory) { + return _createZoneParameters(_extraData, _orderHash, _createMockConsideration(10)); + } + + function _createZoneParameters(bytes memory _extraData, bytes32 _orderHash, ReceivedItem[] memory _consideration) internal view returns (ZoneParameters memory) { + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = _orderHash; + return _createZoneParameters(_extraData, _orderHash, orderHashes, _consideration); + } + + function _createZoneParameters(bytes memory _extraData, bytes32 _orderHash, bytes32[] memory _orderHashes, ReceivedItem[] memory _consideration) internal view returns (ZoneParameters memory) { + return ZoneParameters({ + orderHash: _orderHash, + fulfiller: theFulfiller, + offerer: address(0), + offer: new SpentItem[](0), + consideration: _consideration, + extraData: _extraData, + orderHashes: _orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + } + + function _createMockConsideration(uint256 count) internal returns (ReceivedItem[] memory) { + ReceivedItem[] memory consideration = new ReceivedItem[](count); + for (uint256 i = 0; i < count; i++) { + address payable recipient = payable(makeAddr(string(abi.encodePacked("recipient", vm.toString(i))))); + address payable token = payable(makeAddr(string(abi.encodePacked("token", vm.toString(i))))); + consideration[i] = ReceivedItem({ + itemType: ItemType.NATIVE, + token: token, + identifier: 123, + amount: 12, + recipient: recipient + }); + } + return consideration; + } + + function _convertConsiderationToReceivedItems(ConsiderationItem[] memory _items) internal pure returns (ReceivedItem[] memory) { + ReceivedItem[] memory receivedItems = new ReceivedItem[](_items.length); + for (uint256 i = 0; i < _items.length; i++) { + receivedItems[i] = ReceivedItem({ + itemType: _items[i].itemType, + token: _items[i].token, + identifier: _items[i].identifierOrCriteria, + amount: _items[i].startAmount, + recipient: _items[i].recipient + }); + } + return receivedItems; + } + + function _createConsiderationItems(address recipient, uint256 amount) internal pure returns (ConsiderationItem[] memory) { + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.NATIVE, + token: address(0), + identifierOrCriteria: 0, + startAmount: amount, + endAmount: amount, + recipient: payable(recipient) + }); + return consideration; + } + + function _deriveReceivedItemsHash( + ReceivedItem[] calldata receivedItems + ) public pure returns (bytes32) { + return _deriveReceivedItemsHash(receivedItems, 1, 1); + } + + function _deriveReceivedItemsHash( + ReceivedItem[] calldata receivedItems, + uint256 scalingFactorNumerator, + uint256 scalingFactorDenominator + ) public pure returns (bytes32) { + uint256 numberOfItems = receivedItems.length; + bytes memory receivedItemsHash = new bytes(0); // Explicitly initialize to empty bytes + + for (uint256 i; i < numberOfItems; i++) { + receivedItemsHash = abi.encodePacked( + receivedItemsHash, + receivedItems[i].itemType, + receivedItems[i].token, + receivedItems[i].identifier, + Math.mulDiv(receivedItems[i].amount, scalingFactorNumerator, scalingFactorDenominator), + receivedItems[i].recipient + ); + } + + return keccak256(receivedItemsHash); + } + + function _signOrder(uint256 /* _signerPkey */, bytes32 /* _orderHash */) internal pure returns (bytes memory) { + // For the purposes of testing, the offerer wallet will always return valid for signature checks + return abi.encodePacked("Hello!"); + } + + function _signSIP7Order( + uint256 _signerPkey, + bytes32 orderHash, + uint64 expiration, + bytes memory context + ) internal view returns (bytes memory) { + uint256 chainId = block.chainid; + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(ZONE_NAME)), + keccak256(bytes(VERSION)), + chainId, + theZone + ) + ); + + bytes32 structHash = keccak256( + abi.encode( + keccak256("SignedOrder(address fulfiller,uint64 expiration,bytes32 orderHash,bytes context)"), + theFulfiller, + expiration, + orderHash, + keccak256(context) + ) + ); + + bytes32 digest = ECDSA.toTypedDataHash(domainSeparator, structHash); + + return _signCompact(_signerPkey, digest); + } + + // Helper functions + function _createOfferItems(address token, uint256 tokenId) internal pure returns (OfferItem[] memory) { + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721, + token: token, + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + return offer; + } + + function _generateSip7Signature(bytes32 orderHash, address fulfiller, uint256 signerPkey, uint64 _expiration, ConsiderationItem[] memory _consideration) internal view returns (bytes memory) { + ReceivedItem[] memory receivedItems = _convertConsiderationToReceivedItems(_consideration); + bytes32 substandard3Data = this._deriveReceivedItemsHash(receivedItems); + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = orderHash; + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data); + + bytes memory signature = _signSIP7Order(signerPkey, orderHash, _expiration, context); + return abi.encodePacked( + bytes1(0), + fulfiller, + _expiration, + signature, + context + ); + } +} \ No newline at end of file diff --git a/test/trading/seaport16/README.md b/test/trading/seaport16/README.md new file mode 100644 index 00000000..39c2f0da --- /dev/null +++ b/test/trading/seaport16/README.md @@ -0,0 +1,10 @@ +# Seaport 1.6 Test Code + +The code in this directory is the same as the Seaport 1.5 tests (in [../seaport](../seaport/)), with the following exceptions: + +* All references to Immutable's Seaport implementation are to the 1.6 implementation. +* All references to seaport, seaport-core, and seaport-types are to seaport-16, seaport-core-16, and seaport-types-16. +* All references to test/trading/seaport/utils has been updated to use the version in the seaport 1.5 directory. These files have not changed as they do not reference Seaport. +* Immutable signed zone v2 has been renamed to v3. + + diff --git a/test/trading/seaport16/utils/MockTransferValidator.t.sol b/test/trading/seaport16/utils/MockTransferValidator.t.sol new file mode 100644 index 00000000..c2d129f0 --- /dev/null +++ b/test/trading/seaport16/utils/MockTransferValidator.t.sol @@ -0,0 +1,255 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2025 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ITransferValidator} from "@limitbreak/creator-token-standards/src/interfaces/ITransferValidator.sol"; + +contract MockTransferValidator is ITransferValidator { + bool private _shouldRevertApplyCollectionTransferPolicy = false; + address private _revertApplyCollectionTransferPolicyCaller; + address private _revertApplyCollectionTransferPolicyFrom; + address private _revertApplyCollectionTransferPolicyTo; + + bool private _shouldRevertValidateTransfer = false; + address private _revertValidateTransferCaller; + address private _revertValidateTransferFrom; + address private _revertValidateTransferTo; + + bool private _shouldRevertValidateTransferWithTokenId = false; + address private _revertValidateTransferWithTokenIdCaller; + address private _revertValidateTransferWithTokenIdFrom; + address private _revertValidateTransferWithTokenIdTo; + uint256 private _revertValidateTransferWithTokenIdTokenId; + + bool private _shouldRevertValidateTransferWithTokenIdAndAmount = false; + address private _revertValidateTransferWithTokenIdAndAmountCaller; + address private _revertValidateTransferWithTokenIdAndAmountFrom; + address private _revertValidateTransferWithTokenIdAndAmountTo; + uint256 private _revertValidateTransferWithTokenIdAndAmountTokenId; + uint256 private _revertValidateTransferWithTokenIdAndAmountAmount; + + bool private _shouldRevertBeforeAuthorizedTransferWithOperatorAndTokenId = false; + address private _revertBeforeAuthorizedTransferWithOperatorAndTokenIdOperator; + address private _revertBeforeAuthorizedTransferWithOperatorAndTokenIdToken; + uint256 private _revertBeforeAuthorizedTransferWithOperatorAndTokenIdTokenId; + + bool private _shouldRevertAfterAuthorizedTransferWithTokenId = false; + address private _revertAfterAuthorizedTransferWithTokenIdToken; + uint256 private _revertAfterAuthorizedTransferWithTokenIdTokenId; + + bool private _shouldRevertBeforeAuthorizedTransferWithOperator = false; + address private _revertBeforeAuthorizedTransferWithOperatorOperator; + address private _revertBeforeAuthorizedTransferWithOperatorToken; + + bool private _shouldRevertAfterAuthorizedTransfer = false; + address private _revertAfterAuthorizedTransferToken; + + bool private _shouldRevertBeforeAuthorizedTransferWithTokenId = false; + address private _revertBeforeAuthorizedTransferWithTokenIdToken; + uint256 private _revertBeforeAuthorizedTransferWithTokenIdTokenId; + + bool private _shouldRevertBeforeAuthorizedTransferWithAmount = false; + address private _revertBeforeAuthorizedTransferWithAmountToken; + uint256 private _revertBeforeAuthorizedTransferWithAmountTokenId; + uint256 private _revertBeforeAuthorizedTransferWithAmountAmount; + + bool private _shouldRevertAfterAuthorizedTransferWithAmount = false; + address private _revertAfterAuthorizedTransferWithAmountToken; + uint256 private _revertAfterAuthorizedTransferWithAmountTokenId; + + function revertApplyCollectionTransferPolicy(address caller, address from, address to) public { + _shouldRevertApplyCollectionTransferPolicy = true; + _revertApplyCollectionTransferPolicyCaller = caller; + _revertApplyCollectionTransferPolicyFrom = from; + _revertApplyCollectionTransferPolicyTo = to; + } + + function revertValidateTransfer(address caller, address from, address to) public { + _shouldRevertValidateTransfer = true; + _revertValidateTransferCaller = caller; + _revertValidateTransferFrom = from; + _revertValidateTransferTo = to; + } + + function revertValidateTransferWithTokenId(address caller, address from, address to, uint256 tokenId) public { + _shouldRevertValidateTransferWithTokenId = true; + _revertValidateTransferWithTokenIdCaller = caller; + _revertValidateTransferWithTokenIdFrom = from; + _revertValidateTransferWithTokenIdTo = to; + _revertValidateTransferWithTokenIdTokenId = tokenId; + } + + function revertValidateTransferWithTokenIdAndAmount(address caller, address from, address to, uint256 tokenId, uint256 amount) public { + _shouldRevertValidateTransferWithTokenIdAndAmount = true; + _revertValidateTransferWithTokenIdAndAmountCaller = caller; + _revertValidateTransferWithTokenIdAndAmountFrom = from; + _revertValidateTransferWithTokenIdAndAmountTo = to; + _revertValidateTransferWithTokenIdAndAmountTokenId = tokenId; + _revertValidateTransferWithTokenIdAndAmountAmount = amount; + } + + function revertBeforeAuthorizedTransferWithOperatorAndTokenId(address operator, address token, uint256 tokenId) public { + _shouldRevertBeforeAuthorizedTransferWithOperatorAndTokenId = true; + _revertBeforeAuthorizedTransferWithOperatorAndTokenIdOperator = operator; + _revertBeforeAuthorizedTransferWithOperatorAndTokenIdToken = token; + _revertBeforeAuthorizedTransferWithOperatorAndTokenIdTokenId = tokenId; + } + + function revertAfterAuthorizedTransferWithTokenId(address token, uint256 tokenId) public { + _shouldRevertAfterAuthorizedTransferWithTokenId = true; + _revertAfterAuthorizedTransferWithTokenIdToken = token; + _revertAfterAuthorizedTransferWithTokenIdTokenId = tokenId; + } + + function revertBeforeAuthorizedTransferWithOperator(address operator, address token) public { + _shouldRevertBeforeAuthorizedTransferWithOperator = true; + _revertBeforeAuthorizedTransferWithOperatorOperator = operator; + _revertBeforeAuthorizedTransferWithOperatorToken = token; + } + + function revertAfterAuthorizedTransfer(address token) public { + _shouldRevertAfterAuthorizedTransfer = true; + _revertAfterAuthorizedTransferToken = token; + } + + function revertBeforeAuthorizedTransferWithTokenId(address token, uint256 tokenId) public { + _shouldRevertBeforeAuthorizedTransferWithTokenId = true; + _revertBeforeAuthorizedTransferWithTokenIdToken = token; + _revertBeforeAuthorizedTransferWithTokenIdTokenId = tokenId; + } + + function revertBeforeAuthorizedTransferWithAmount(address token, uint256 tokenId, uint256 amount) public { + _shouldRevertBeforeAuthorizedTransferWithAmount = true; + _revertBeforeAuthorizedTransferWithAmountToken = token; + _revertBeforeAuthorizedTransferWithAmountTokenId = tokenId; + _revertBeforeAuthorizedTransferWithAmountAmount = amount; + } + + function revertAfterAuthorizedTransferWithAmount(address token, uint256 tokenId) public { + _shouldRevertAfterAuthorizedTransferWithAmount = true; + _revertAfterAuthorizedTransferWithAmountToken = token; + _revertAfterAuthorizedTransferWithAmountTokenId = tokenId; + } + + function applyCollectionTransferPolicy(address caller, address from, address to) external view override { + if ( + _shouldRevertApplyCollectionTransferPolicy && + caller == _revertApplyCollectionTransferPolicyCaller && + from == _revertApplyCollectionTransferPolicyFrom && + to == _revertApplyCollectionTransferPolicyTo + ) { + revert MockTransferValidatorRevert("applyCollectionTransferPolicy(address caller, address from, address to)"); + } + } + + function validateTransfer(address caller, address from, address to) external view override { + if ( + _shouldRevertValidateTransfer && + caller == _revertValidateTransferCaller && + from == _revertValidateTransferFrom && + to == _revertValidateTransferTo + ) { + revert MockTransferValidatorRevert("validateTransfer(address caller, address from, address to)"); + } + } + + function validateTransfer(address caller, address from, address to, uint256 tokenId) external view override { + if ( + _shouldRevertValidateTransferWithTokenId && + caller == _revertValidateTransferWithTokenIdCaller && + from == _revertValidateTransferWithTokenIdFrom && + to == _revertValidateTransferWithTokenIdTo && + tokenId == _revertValidateTransferWithTokenIdTokenId + ) { + revert MockTransferValidatorRevert("validateTransfer(address caller, address from, address to, uint256 tokenId)"); + } + } + + function validateTransfer(address caller, address from, address to, uint256 tokenId, uint256 amount) external view override { + if ( + _shouldRevertValidateTransferWithTokenIdAndAmount && + caller == _revertValidateTransferWithTokenIdAndAmountCaller && + from == _revertValidateTransferWithTokenIdAndAmountFrom && + to == _revertValidateTransferWithTokenIdAndAmountTo && + tokenId == _revertValidateTransferWithTokenIdAndAmountTokenId && + amount == _revertValidateTransferWithTokenIdAndAmountAmount + ) { + revert MockTransferValidatorRevert("validateTransfer(address caller, address from, address to, uint256 tokenId, uint256 amount)"); + } + } + + function beforeAuthorizedTransfer(address operator, address token, uint256 tokenId) external view override { + if ( + _shouldRevertBeforeAuthorizedTransferWithOperatorAndTokenId && + operator == _revertBeforeAuthorizedTransferWithOperatorAndTokenIdOperator && + token == _revertBeforeAuthorizedTransferWithOperatorAndTokenIdToken && + tokenId == _revertBeforeAuthorizedTransferWithOperatorAndTokenIdTokenId + ) { + revert MockTransferValidatorRevert("beforeAuthorizedTransfer(address operator, address token, uint256 tokenId)"); + } + } + + function afterAuthorizedTransfer(address token, uint256 tokenId) external view override { + if ( + _shouldRevertAfterAuthorizedTransferWithTokenId && + token == _revertAfterAuthorizedTransferWithTokenIdToken && + tokenId == _revertAfterAuthorizedTransferWithTokenIdTokenId + ) { + revert MockTransferValidatorRevert("afterAuthorizedTransfer(address token, uint256 tokenId)"); + } + } + + function beforeAuthorizedTransfer(address operator, address token) external view override { + if ( + _shouldRevertBeforeAuthorizedTransferWithOperator && + operator == _revertBeforeAuthorizedTransferWithOperatorOperator && + token == _revertBeforeAuthorizedTransferWithOperatorToken + ) { + revert MockTransferValidatorRevert("beforeAuthorizedTransfer(address operator, address token)"); + } + } + + function afterAuthorizedTransfer(address token) external view override { + if ( + _shouldRevertAfterAuthorizedTransfer && + token == _revertAfterAuthorizedTransferToken + ) { + revert MockTransferValidatorRevert("afterAuthorizedTransfer(address token)"); + } + } + + function beforeAuthorizedTransfer(address token, uint256 tokenId) external view override { + if ( + _shouldRevertBeforeAuthorizedTransferWithTokenId && + token == _revertBeforeAuthorizedTransferWithTokenIdToken && + tokenId == _revertBeforeAuthorizedTransferWithTokenIdTokenId + ) { + revert MockTransferValidatorRevert("beforeAuthorizedTransfer(address token, uint256 tokenId)"); + } + } + + function beforeAuthorizedTransferWithAmount(address token, uint256 tokenId, uint256 amount) external view override { + if ( + _shouldRevertBeforeAuthorizedTransferWithAmount && + token == _revertBeforeAuthorizedTransferWithAmountToken && + tokenId == _revertBeforeAuthorizedTransferWithAmountTokenId && + amount == _revertBeforeAuthorizedTransferWithAmountAmount + ) { + revert MockTransferValidatorRevert("beforeAuthorizedTransferWithAmount(address token, uint256 tokenId, uint256 amount)"); + } + } + + function afterAuthorizedTransferWithAmount(address token, uint256 tokenId) external view override { + if ( + _shouldRevertAfterAuthorizedTransferWithAmount && + token == _revertAfterAuthorizedTransferWithAmountToken && + tokenId == _revertAfterAuthorizedTransferWithAmountTokenId + ) { + revert MockTransferValidatorRevert("afterAuthorizedTransferWithAmount(address token, uint256 tokenId)"); + } + } +} + +error MockTransferValidatorRevert(string functionDefinition); diff --git a/test/trading/seaport16/utils/SigningTestHelper.t.sol b/test/trading/seaport16/utils/SigningTestHelper.t.sol new file mode 100644 index 00000000..33f56497 --- /dev/null +++ b/test/trading/seaport16/utils/SigningTestHelper.t.sol @@ -0,0 +1,24 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +// solhint-disable-next-line no-global-import +import "forge-std/Test.sol"; + +abstract contract SigningTestHelper is Test { + function _sign(uint256 signerPrivateKey, bytes32 signatureDigest) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, signatureDigest); + return abi.encodePacked(r, s, v); + } + + function _signCompact(uint256 signerPrivateKey, bytes32 signatureDigest) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, signatureDigest); + if (v != 27) { + // then left-most bit of s has to be flipped to 1. + s = s | bytes32(uint256(1) << 255); + } + return abi.encodePacked(r, s); + } +} diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol new file mode 100644 index 00000000..e051556e --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol @@ -0,0 +1,71 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2025 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ZoneInterface} from "seaport-16/contracts/interfaces/ZoneInterface.sol"; +import {ReceivedItem, ZoneParameters} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {SIP7Interface} from "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7Interface.sol"; + +// solhint-disable func-name-mixedcase + +interface IImmutableSignedZoneV3Harness is ZoneInterface, SIP7Interface { + function grantRole(bytes32 role, address account) external; + + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + + function ZONE_MANAGER_ROLE() external view returns (bytes32); + + function exposed_domainSeparator() external view returns (bytes32); + + function exposed_deriveDomainSeparator() external view returns (bytes32 domainSeparator); + + function exposed_getSupportedSubstandards() external pure returns (uint256[] memory substandards); + + function exposed_deriveSignedOrderHash( + address fulfiller, + uint64 expiration, + bytes32 orderHash, + bytes calldata context + ) external view returns (bytes32 signedOrderHash); + + function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external; + + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256); + + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256); + + function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256); + + function exposed_validateSubstandard7(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + returns (uint256); + + function exposed_validateSubstandard8(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + returns (uint256); + + function exposed_deriveReceivedItemsHash( + ReceivedItem[] calldata receivedItems, + uint256 scalingFactorNumerator, + uint256 scalingFactorDenominator + ) external pure returns (bytes32); + + function exposed_bytes32ArrayIncludes(bytes32[] calldata sourceArray, bytes32[] memory values) + external + pure + returns (bool); +} + +// solhint-enable func-name-mixedcase diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol new file mode 100644 index 00000000..42f210e4 --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -0,0 +1,2255 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2025 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +// solhint-disable-next-line no-global-import +import "forge-std/Test.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ItemType} from "seaport-types-16/src/lib/ConsiderationEnums.sol"; +import {ReceivedItem, Schema, SpentItem, ZoneParameters} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {ImmutableSignedZoneV3} from + "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol"; +import {SIP5EventsAndErrors} from + "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5EventsAndErrors.sol"; +import {SIP6EventsAndErrors} from + "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6EventsAndErrors.sol"; +import {SIP7EventsAndErrors} from + "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol"; +import {ZoneAccessControlEventsAndErrors} from + "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/ZoneAccessControlEventsAndErrors.sol"; +import {SigningTestHelper} from "../../../../seaport/utils/SigningTestHelper.t.sol"; +import {ImmutableSignedZoneV3Harness} from "./ImmutableSignedZoneV3Harness.t.sol"; +import {MockTransferValidator, MockTransferValidatorRevert} from "../../../utils/MockTransferValidator.t.sol"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSignedZoneV3Test is + Test, + SigningTestHelper, + ZoneAccessControlEventsAndErrors, + SIP5EventsAndErrors, + SIP6EventsAndErrors, + SIP7EventsAndErrors +{ + // solhint-disable private-vars-leading-underscore + uint64 private constant DEFAULT_EXPIRATION = 100; + address private immutable OWNER = makeAddr("owner"); + address private immutable FULFILLER = makeAddr("fulfiller"); + address private immutable OFFERER = makeAddr("offerer"); + address private immutable SIGNER; + uint256 private immutable SIGNER_PRIVATE_KEY; + address private immutable SEAPORT = makeAddr("seaport"); + // solhint-enable private-vars-leading-underscore + + // OpenZeppelin v5 access/IAccessControl.sol + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + error AccessControlBadConfirmation(); + + constructor() { + (SIGNER, SIGNER_PRIVATE_KEY) = makeAddrAndKey("signer"); + } + + /* constructor */ + + function test_contructor_grantsAdminRoleToOwner() public { + address owner = makeAddr("owner"); + ImmutableSignedZoneV3 zone = new ImmutableSignedZoneV3( + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", owner + ); + bool ownerHasAdminRole = zone.hasRole(zone.DEFAULT_ADMIN_ROLE(), owner); + assertTrue(ownerHasAdminRole); + } + + function test_contructor_emitsSeaportCompatibleContractDeployedEvent() public { + vm.expectEmit(); + emit SeaportCompatibleContractDeployed(); + new ImmutableSignedZoneV3( + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", makeAddr("owner") + ); + } + + /* grantRole */ + + function test_grantRole_revertsIfCalledByNonAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + address nonAdmin = makeAddr("non_admin"); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, nonAdmin, zone.DEFAULT_ADMIN_ROLE()) + ); + vm.prank(nonAdmin); + zone.grantRole(managerRole, OWNER); + } + + function test_grantRole_grantsIfCalledByAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + address newManager = makeAddr("new_manager"); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, newManager); + bool newManagerHasManagerRole = zone.hasRole(managerRole, newManager); + assertTrue(newManagerHasManagerRole); + } + + /* revokeRole */ + + function test_revokeRole_revertsIfCalledByNonAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + address managerOne = makeAddr("manager_one"); + address managerTwo = makeAddr("manager_two"); + vm.prank(OWNER); + zone.grantRole(managerRole, managerOne); + vm.prank(OWNER); + zone.grantRole(managerRole, managerTwo); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, managerOne, zone.DEFAULT_ADMIN_ROLE()) + ); + vm.prank(managerOne); + zone.revokeRole(managerRole, managerTwo); + } + + function test_revokeRole_revertsIfRevokingLastDefaultAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 adminRole = zone.DEFAULT_ADMIN_ROLE(); + vm.expectRevert(abi.encodeWithSelector(LastDefaultAdminRole.selector, OWNER)); + vm.prank(OWNER); + zone.revokeRole(adminRole, OWNER); + } + + function test_revokeRole_revokesIfRevokingNonLastDefaultAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 adminRole = zone.DEFAULT_ADMIN_ROLE(); + address newAdmin = makeAddr("new_admin"); + vm.prank(OWNER); + zone.grantRole(adminRole, newAdmin); + vm.prank(OWNER); + zone.revokeRole(adminRole, OWNER); + bool ownerHasAdminRole = zone.hasRole(adminRole, OWNER); + assertFalse(ownerHasAdminRole); + } + + function test_revokeRole_revokesIfRevokingLastNonDefaultAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.revokeRole(managerRole, OWNER); + bool ownerHasManagerRole = zone.hasRole(managerRole, OWNER); + uint256 managerCount = zone.getRoleMemberCount(managerRole); + assertFalse(ownerHasManagerRole); + assertEq(managerCount, 0); + } + + /* renounceRole */ + + function test_renounceRole_revertsIfCallerDoesNotMatchCallerConfirmationAddress() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + address newManager = makeAddr("new_manager"); + vm.prank(OWNER); + zone.grantRole(managerRole, newManager); + vm.expectRevert(abi.encodeWithSelector(AccessControlBadConfirmation.selector)); + vm.prank(newManager); + zone.renounceRole(managerRole, makeAddr("random")); + } + + function test_renounceRole_revertsIfRenouncingLastDefaultAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 adminRole = zone.DEFAULT_ADMIN_ROLE(); + vm.expectRevert(abi.encodeWithSelector(LastDefaultAdminRole.selector, OWNER)); + vm.prank(OWNER); + zone.renounceRole(adminRole, OWNER); + } + + function test_renounceRole_revokesIfRenouncingNonLastDefaultAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 adminRole = zone.DEFAULT_ADMIN_ROLE(); + address newAdmin = makeAddr("new_admin"); + vm.prank(OWNER); + zone.grantRole(adminRole, newAdmin); + vm.prank(OWNER); + zone.renounceRole(adminRole, OWNER); + bool ownerHasAdminRole = zone.hasRole(adminRole, OWNER); + assertFalse(ownerHasAdminRole); + } + + function test_renounceRole_revokesIfRenouncingLastNonDefaultAdminRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.renounceRole(managerRole, OWNER); + bool ownerHasManagerRole = zone.hasRole(managerRole, OWNER); + uint256 managerCount = zone.getRoleMemberCount(managerRole); + assertFalse(ownerHasManagerRole); + assertEq(managerCount, 0); + } + + /* addSigner */ + + function test_addSigner_revertsIfCalledByNonZoneManagerRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, OWNER, zone.ZONE_MANAGER_ROLE()) + ); + vm.prank(OWNER); + zone.addSigner(makeAddr("signer_to_add")); + } + + function test_addSigner_revertsIfSignerIsTheZeroAddress() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.expectRevert(abi.encodeWithSelector(SignerCannotBeZeroAddress.selector)); + vm.prank(OWNER); + zone.addSigner(address(0)); + } + + function test_addSigner_emitsSignerAddedEvent() public { + address signerToAdd = makeAddr("signer_to_add"); + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.expectEmit(address(zone)); + emit SignerAdded(signerToAdd); + vm.prank(OWNER); + zone.addSigner(signerToAdd); + } + + function test_addSigner_revertsIfSignerAlreadyActive() public { + address signerToAdd = makeAddr("signer_to_add"); + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(signerToAdd); + vm.expectRevert(abi.encodeWithSelector(SignerAlreadyActive.selector, signerToAdd)); + vm.prank(OWNER); + zone.addSigner(signerToAdd); + } + + function test_addSigner_revertsIfSignerWasPreviouslyActive() public { + address signerToAdd = makeAddr("signer_to_add"); + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(signerToAdd); + vm.prank(OWNER); + zone.removeSigner(signerToAdd); + vm.expectRevert(abi.encodeWithSelector(SignerCannotBeReauthorized.selector, signerToAdd)); + vm.prank(OWNER); + zone.addSigner(signerToAdd); + } + + /* removeSigner */ + + function test_removeSigner_revertsIfCalledByNonZoneManagerRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, OWNER, zone.ZONE_MANAGER_ROLE()) + ); + vm.prank(OWNER); + zone.removeSigner(makeAddr("signer_to_remove")); + } + + function test_removeSigner_revertsIfSignerNotActive() public { + address signerToRemove = makeAddr("signer_to_remove"); + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.expectRevert(abi.encodeWithSelector(SignerNotActive.selector, signerToRemove)); + vm.prank(OWNER); + zone.removeSigner(signerToRemove); + } + + function test_removeSigner_emitsSignerRemovedEvent() public { + address signerToRemove = makeAddr("signer_to_remove"); + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(signerToRemove); + vm.expectEmit(address(zone)); + emit SignerRemoved(signerToRemove); + vm.prank(OWNER); + zone.removeSigner(signerToRemove); + } + + /* updateAPIEndpoint */ + + function test_updateAPIEndpoint_revertsIfCalledByNonZoneManagerRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, OWNER, zone.ZONE_MANAGER_ROLE()) + ); + vm.prank(OWNER); + zone.updateAPIEndpoint("https://www.new-immutable.com"); + } + + function test_updateAPIEndpoint_updatesAPIEndpointIfCalledByZoneManagerRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + string memory expectedApiEndpoint = "https://www.new-immutable.com"; + vm.prank(OWNER); + zone.updateAPIEndpoint(expectedApiEndpoint); + (, Schema[] memory schemas) = zone.getSeaportMetadata(); + (, string memory apiEndpoint,,) = abi.decode(schemas[0].metadata, (bytes32, string, uint256[], string)); + assertEq(apiEndpoint, expectedApiEndpoint); + } + + /* updateDocumentationURI */ + + function test_updateDocumentationURI_revertsIfCalledByNonZoneManagerRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + vm.expectRevert( + abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, OWNER, zone.ZONE_MANAGER_ROLE()) + ); + vm.prank(OWNER); + zone.updateDocumentationURI("https://www.new-immutable.com/docs"); + } + + function test_updateDocumentationURI_updatesDocumentationURIIfCalledByZoneManagerRole() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + string memory expectedDocumentationURI = "https://www.new-immutable.com/docs"; + vm.prank(OWNER); + zone.updateDocumentationURI(expectedDocumentationURI); + (, Schema[] memory schemas) = zone.getSeaportMetadata(); + (,,, string memory documentationURI) = abi.decode(schemas[0].metadata, (bytes32, string, uint256[], string)); + assertEq(documentationURI, expectedDocumentationURI); + } + + /* getSeaportMetadata */ + + function test_getSeaportMetadata() public { + string memory expectedZoneName = "MyZoneName"; + string memory expectedApiEndpoint = "https://www.immutable.com"; + string memory expectedDocumentationURI = "https://www.immutable.com/docs"; + + ImmutableSignedZoneV3Harness zone = + new ImmutableSignedZoneV3Harness(expectedZoneName, SEAPORT, expectedApiEndpoint, expectedDocumentationURI, OWNER); + + bytes32 expectedDomainSeparator = zone.exposed_deriveDomainSeparator(); + uint256[] memory expectedSubstandards = zone.exposed_getSupportedSubstandards(); + + (string memory name, Schema[] memory schemas) = zone.getSeaportMetadata(); + ( + bytes32 domainSeparator, + string memory apiEndpoint, + uint256[] memory substandards, + string memory documentationURI + ) = abi.decode(schemas[0].metadata, (bytes32, string, uint256[], string)); + + assertEq(name, expectedZoneName); + assertEq(schemas.length, 1); + assertEq(schemas[0].id, 7); + assertEq(domainSeparator, expectedDomainSeparator); + assertEq(apiEndpoint, expectedApiEndpoint); + assertEq(substandards, expectedSubstandards); + assertEq(documentationURI, expectedDocumentationURI); + } + + /* sip7Information */ + + function test_sip7Information() public { + string memory expectedApiEndpoint = "https://www.immutable.com"; + string memory expectedDocumentationURI = "https://www.immutable.com/docs"; + + ImmutableSignedZoneV3Harness zone = + new ImmutableSignedZoneV3Harness("MyZoneName", SEAPORT, expectedApiEndpoint, expectedDocumentationURI, OWNER); + + bytes32 expectedDomainSeparator = zone.exposed_deriveDomainSeparator(); + uint256[] memory expectedSubstandards = zone.exposed_getSupportedSubstandards(); + + ( + bytes32 domainSeparator, + string memory apiEndpoint, + uint256[] memory substandards, + string memory documentationURI + ) = zone.sip7Information(); + + assertEq(domainSeparator, expectedDomainSeparator); + assertEq(apiEndpoint, expectedApiEndpoint); + assertEq(substandards, expectedSubstandards); + assertEq(documentationURI, expectedDocumentationURI); + } + + /* authorizeOrder */ + + function test_authorizeOrder_revertsIfSeaportNotCaller() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.expectRevert(abi.encodeWithSelector(CallerNotSeaport.selector)); + vm.prank(makeAddr("not_seaport")); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_allowedIfSeaportIsCaller() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfEmptyExtraData() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = new bytes(0); + vm.expectRevert( + abi.encodeWithSelector(InvalidExtraData.selector, "extraData is empty", zoneParameters.orderHash) + ); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfExtraDataLengthIsLessThan93() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = bytes(hex"01"); + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "extraData length must be at least 93 bytes", zoneParameters.orderHash + ) + ); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfExtraDataVersionIsNotSupported() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = bytes( + hex"01f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000660f3027d9ef9e6e50a74cc24433373b9cdd97693a02adcc94e562bb59a5af68190ecaea4414dcbe74618f6c77d11cbcf4a8345bbdf46e665249904925c95929ba6606638b779c6b502204fca6bb0539cdc3dc258fe3ce7b53be0c4ad620899167fedaa8" + ); + vm.expectRevert(abi.encodeWithSelector(UnsupportedExtraDataVersion.selector, uint8(1))); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfSignatureHasExpired() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + SignatureExpired.selector, + 1000, + 100, + zoneParameters.orderHash + ) + ); + // set current block.timestamp to be 1000 + vm.warp(1000); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.fulfiller = makeAddr("random"); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + FULFILLER, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidFulfiller.selector, + FULFILLER, + zoneParameters.fulfiller, + zoneParameters.orderHash + ) + ); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfNoSpentItems() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.offer = new SpentItem[](0); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + NoSpentItems.selector, + zoneParameters.orderHash + ) + ); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfNoReceivedItems() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.consideration = new ReceivedItem[](0); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + NoReceivedItems.selector, + zoneParameters.orderHash + ) + ); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_revertsIfSignerIsNotActive() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + // no signer added + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.expectRevert( + abi.encodeWithSelector(SignerNotActive.selector, address(0x6E12D8C87503D4287c294f2Fdef96ACd9DFf6bd2)) + ); + vm.prank(SEAPORT); + zone.authorizeOrder(zoneParameters); + } + + function test_authorizeOrder_returnsMagicValueOnSuccessfulValidation() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.prank(SEAPORT); + assertEq(zone.authorizeOrder(zoneParameters), bytes4(0x01e4d72a)); + } + + /* validateOrder */ + + function test_validateOrder_revertsIfSeaportNotCaller() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.expectRevert(abi.encodeWithSelector(CallerNotSeaport.selector)); + vm.prank(makeAddr("not_seaport")); + zone.validateOrder(zoneParameters); + } + + function test_validateOrder_allowedIfSeaportIsCaller() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.prank(SEAPORT); + zone.validateOrder(zoneParameters); + } + + function test_validateOrder_revertsIfContextIsEmpty() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash + ) + ); + vm.prank(SEAPORT); + zone.validateOrder(zoneParameters); + } + + function test_validateOrder_returnsMagicValueOnSuccessfulValidation() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(SIGNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) + ); + + vm.prank(SEAPORT); + assertEq(zone.validateOrder(zoneParameters), bytes4(0x17b1f942)); + } + + /* supportsInterface */ + + function test_supportsInterface() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + assertTrue(zone.supportsInterface(0x01ffc9a7)); // ERC165 interface + assertFalse(zone.supportsInterface(0xffffffff)); // ERC165 compliance + assertTrue(zone.supportsInterface(0x2e778efc)); // SIP-5 interface + assertTrue(zone.supportsInterface(0x39dd6933)); // SIP-5 compliance - ZoneInterface + } + + /* _domainSeparator */ + + function test_domainSeparator_returnsCachedDomainSeparatorWhenChainIDMatchesValueSetOnDeployment() public { + address deployer = makeAddr("deployer"); + vm.startPrank(deployer); + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + vm.stopPrank(); + + bytes32 domainSeparator = zone.exposed_domainSeparator(); + assertEq(domainSeparator, bytes32(0x7946de4e45fec74bd718eeaf23c0d6e9ee4a66d31dcaa416695b27a794624c58)); + } + + function test_domainSeparator_returnsUpdatedDomainSeparatorIfChainIDIsDifferentFromValueSetOnDeployment() public { + address deployer = makeAddr("deployer"); + vm.startPrank(deployer); + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + vm.stopPrank(); + + bytes32 domainSeparatorCached = zone.exposed_domainSeparator(); + vm.chainId(31338); + bytes32 domainSeparatorDerived = zone.exposed_domainSeparator(); + + assertNotEq(domainSeparatorCached, domainSeparatorDerived); + assertEq(domainSeparatorDerived, bytes32(0xf0f125b29a5274c4bcd4a916a5363c1c79472ac09bca6f09dbb97a23a3e6bc8f)); + } + + /* _deriveDomainSeparator */ + + function test_deriveDomainSeparator_returnsDomainSeparatorForChainID() public { + address deployer = makeAddr("deployer"); + vm.startPrank(deployer); + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + vm.stopPrank(); + + bytes32 domainSeparator = zone.exposed_deriveDomainSeparator(); + assertEq(domainSeparator, bytes32(0x7946de4e45fec74bd718eeaf23c0d6e9ee4a66d31dcaa416695b27a794624c58)); + } + + /* _getSupportedSubstandards */ + + function test_getSupportedSubstandards() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + uint256[] memory supportedSubstandards = zone.exposed_getSupportedSubstandards(); + assertEq(supportedSubstandards.length, 6); + assertEq(supportedSubstandards[0], 1); + assertEq(supportedSubstandards[1], 3); + assertEq(supportedSubstandards[2], 4); + assertEq(supportedSubstandards[3], 6); + assertEq(supportedSubstandards[4], 7); + assertEq(supportedSubstandards[5], 8); + } + + /* _deriveSignedOrderHash */ + + function test_deriveSignedOrderHash_returnsHashOfSignedOrder() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + address fulfiller = 0x71458637cD221877830A21F543E8b731e93C3627; + uint64 expiration = 1234995; + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + bytes memory context = hex"9062b0574be745508bed2ff7f8f5057446b89d16d35980b2a26f8e4cb03ddf91"; + bytes32 derivedSignedOrderHash = zone.exposed_deriveSignedOrderHash(fulfiller, expiration, orderHash, context); + assertEq(derivedSignedOrderHash, 0x40c87207c5a0c362da24cb974859c70655de00fee9400f3a805ac360b90bd8c5); + } + + /* _validateSubstandards */ + + function test_validateSubstandards_revertsIfEmptyContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = new bytes(0); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash + ) + ); + + zone.exposed_validateSubstandards(context, zoneParameters, true); + } + + function test_validateSubstandards_beforeHookSubstandard1() public { + _test_validateSubstandards_substandard1(true); + } + + function test_validateSubstandards_afterHookSubstandard1() public { + _test_validateSubstandards_substandard1(false); + } + + function _test_validateSubstandards_substandard1(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard3() public { + _test_validateSubstandards_substandard3(true); + } + + function test_validateSubstandards_afterHookSubstandard3() public { + _test_validateSubstandards_substandard3(false); + } + + function _test_validateSubstandards_substandard3(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes32 substandard3Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard4() public { + _test_validateSubstandards_substandard4(true); + } + + function test_validateSubstandards_afterHookSubstandard4() public { + _test_validateSubstandards_substandard4(false); + } + + function _test_validateSubstandards_substandard4(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory substandard4Data = abi.encode(zoneParameters.orderHashes); + bytes memory context = abi.encodePacked(bytes1(0x04), substandard4Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard6() public { + _test_validateSubstandards_substandard6(true); + } + + function test_validateSubstandards_afterHookSubstandard6() public { + _test_validateSubstandards_substandard6(false); + } + + function _test_validateSubstandards_substandard6(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x2), identifier: 222, amount: 10}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 100, 10); + bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), substandard6Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard7() public { + _test_validateSubstandards_substandard7(true); + } + + function test_validateSubstandards_afterHookSubstandard7() public { + _test_validateSubstandards_substandard7(false); + } + + function _test_validateSubstandards_substandard7(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100 + }); + spentItems[0] = spentItem; + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x07), zoneParameters.consideration[0].identifier, address(transferValidator), address(0x7)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard8() public { + _test_validateSubstandards_substandard8(true); + } + + function test_validateSubstandards_afterHookSubstandard8() public { + _test_validateSubstandards_substandard8(false); + } + + function _test_validateSubstandards_substandard8(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100 + }); + spentItems[0] = spentItem; + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x08), zoneParameters.consideration[0].identifier, address(transferValidator)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookMultipleSubstandardsInCorrectOrder() public { + _test_validateSubstandards_multipleSubstandardsInCorrectOrder(true); + } + + function test_validateSubstandards_afterHookMultipleSubstandardsInCorrectOrder() public { + _test_validateSubstandards_multipleSubstandardsInCorrectOrder(false); + } + + function _test_validateSubstandards_multipleSubstandardsInCorrectOrder(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes32 substandard3Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1); + bytes memory substandard4Data = abi.encode(zoneParameters.orderHashes); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandards3Then6() public { + _test_validateSubstandards_substandards3Then6(true); + } + + function test_validateSubstandards_afterHookSubstandards3Then6() public { + _test_validateSubstandards_substandards3Then6(false); + } + + function _test_validateSubstandards_substandards3Then6(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x4), + identifier: 0, + amount: 20, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes32 substandard3Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x06), substandard6Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookManySubstandards() public { + _test_validateSubstandards_manySubstandards(true); + } + + function test_validateSubstandards_afterHookManySubstandards() public { + _test_validateSubstandards_manySubstandards(false); + } + + function _test_validateSubstandards_manySubstandards(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x4), + identifier: 0, + amount: 20, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes memory substandard1Data = abi.encodePacked(uint256(0)); + bytes32 substandard3Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1); + bytes memory substandard4Data = abi.encode(zoneParameters.orderHashes); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked( + bytes1(0x01), substandard1Data, bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + ); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_revertsOnMultipleSubstandardsInIncorrectOrder() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes32 substandard3Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1); + bytes memory substandard4Data = abi.encode(zoneParameters.orderHashes); + bytes memory context = abi.encodePacked(bytes1(0x04), substandard4Data, bytes1(0x03), substandard3Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid context, unexpected context length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandards(context, zoneParameters, true); + } + + /* _validateSubstandard1 */ + + function test_validateSubstandard1_returnsZeroLengthIfNotSubstandard1() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x03)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(context, zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard1_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x01), bytes10(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 1 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard1(context, zoneParameters, true); + } + + function test_validateSubstandard1_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC20, token: address(0x45), identifier: 0, amount: 200}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x2), + identifier: 45, + amount: 1, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x01), uint256(46)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert(abi.encodeWithSelector(Substandard1Violation.selector, zoneParameters.orderHash, 45, 46)); + zone.exposed_validateSubstandard1(context, zoneParameters, true); + } + + function test_validateSubstandard1_beforeHookReturns33OnSuccess() public { + _test_validateSubstandard1_returns33OnSuccess(true); + } + + function test_validateSubstandard1_afterHookReturns33OnSuccess() public { + _test_validateSubstandard1_returns33OnSuccess(false); + } + + function _test_validateSubstandard1_returns33OnSuccess(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC20, token: address(0x45), identifier: 0, amount: 200}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x2), + identifier: 45, + amount: 1, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x01), uint256(45)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(context, zoneParameters, before); + assertEq(substandardLengthResult, 33); + } + + /* _validateSubstandard3 */ + + function test_validateSubstandard3_returnsZeroLengthIfNotSubstandard3() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x04)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(context, zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard3_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x03), bytes10(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 3 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard3(context, zoneParameters, true); + } + + function test_validateSubstandard3_revertsIfDerivedReceivedItemsHashNotEqualToHashInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x03), bytes32(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert(abi.encodeWithSelector(Substandard3Violation.selector, zoneParameters.orderHash)); + zone.exposed_validateSubstandard3(context, zoneParameters, true); + } + + function test_validateSubstandard3_beforeHookReturns33OnSuccess() public { + _test_validateSubstandard3_returns33OnSuccess(true); + } + + function test_validateSubstandard3_afterHookReturns33OnSuccess() public { + _test_validateSubstandard3_returns33OnSuccess(false); + } + + function _test_validateSubstandard3_returns33OnSuccess(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes32 substandard3Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(context, zoneParameters, before); + assertEq(substandardLengthResult, 33); + } + + /* _validateSubstandard4 */ + + function test_validateSubstandard4_returnsZeroLengthIfNotSubstandard4() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x02)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(context, zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard4_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes memory context = abi.encodePacked(bytes1(0x04), bytes10(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 4 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard4(context, zoneParameters, true); + } + + function test_validateSubstandard4_revertsIfExpectedOrderHashesAreNotPresent() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + zoneParameters.orderHashes = orderHashes; + + bytes32[] memory expectedOrderHashes = new bytes32[](1); + expectedOrderHashes[0] = bytes32(0x17d4cf2b6c174a86b533210b50ba676a82e5ab1e2e89ea538f0a43a37f92fcbf); + + bytes memory context = abi.encodePacked(bytes1(0x04), abi.encode(expectedOrderHashes)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + Substandard4Violation.selector, + zoneParameters.orderHashes, + expectedOrderHashes, + zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard4(context, zoneParameters, false); + } + + function test_validateSubstandard4_beforeHookReturnsLengthOfSubstandardSegmentOnSuccess() public { + _test_validateSubstandard4_returnsLengthOfSubstandardSegmentOnSuccess(true); + } + + function test_validateSubstandard4_afterHookReturnsLengthOfSubstandardSegmentOnSuccess() public { + _test_validateSubstandard4_returnsLengthOfSubstandardSegmentOnSuccess(false); + } + + function _test_validateSubstandard4_returnsLengthOfSubstandardSegmentOnSuccess(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x04), abi.encode(zoneParameters.orderHashes)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(context, zoneParameters, before); + // bytes1 + bytes32 + bytes32 + bytes32 = 97 + assertEq(substandardLengthResult, 97); + } + + /* _validateSubstandard6 */ + + function test_validateSubstandard6_returnsZeroLengthIfNotSubstandard6() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x04)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(context, zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard6_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x06), bytes10(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 6 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard6(context, zoneParameters, true); + } + + function test_validateSubstandard6_revertsIfDerivedReceivedItemsHashesIsNotEqualToHashesInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), bytes32(uint256(0x123456))); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector(Substandard6Violation.selector, zoneParameters.offer[0].amount, 100, zoneParameters.orderHash) + ); + zone.exposed_validateSubstandard6(context, zoneParameters, true); + } + + function test_validateSubstandard6_beforeHookReturnsLengthOfSubstandardSegmentOnSuccess() public { + _test_validateSubstandard6_returnsLengthOfSubstandardSegmentOnSuccess(true); + } + + function test_validateSubstandard6_afterHookReturnsLengthOfSubstandardSegmentOnSuccess() public { + _test_validateSubstandard6_returnsLengthOfSubstandardSegmentOnSuccess(false); + } + + function _test_validateSubstandard6_returnsLengthOfSubstandardSegmentOnSuccess(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x2), identifier: 222, amount: 10}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes32 substandard6Data = zone.exposed_deriveReceivedItemsHash(zoneParameters.consideration, 100, 10); + bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), substandard6Data); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(context, zoneParameters, before); + // bytes1 + uint256 + bytes32 = 65 + assertEq(substandardLengthResult, 65); + } + + /* _validateSubstandard7 */ + + function test_validateSubstandard7_returnsZeroLengthIfNotSubstandard7() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes memory context = abi.encodePacked(bytes1(0x04)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard7(context, zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard7_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes memory context = abi.encodePacked(bytes1(0x07), bytes10(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 7 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard7(context, zoneParameters, true); + } + + function test_validateSubstandard7_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC20, token: address(0x55), identifier: 0, amount: 100}); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x2), + identifier: 45, + amount: 1, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(46), address(0x6), address(0x7)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert(abi.encodeWithSelector(Substandard7IdentifierViolation.selector, zoneParameters.orderHash, 45, 46)); + zone.exposed_validateSubstandard7(context, zoneParameters, true); + } + + function test_validateSubstandard7_revertsIfItemTypeRequirementsAreNotMet() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 0, + amount: 50 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(0x6), address(0x7)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert(abi.encodeWithSelector(Substandard7UnexpectedItemTypeViolation.selector, zoneParameters.orderHash)); + zone.exposed_validateSubstandard7(context, zoneParameters, true); + } + + function test_validateSubstandard7_revertsIfTransferValidatorBeforeAuthorizedTransferReverts() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + address operator = makeAddr("operator"); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x07), zoneParameters.consideration[0].identifier, address(transferValidator), operator); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + transferValidator.revertBeforeAuthorizedTransferWithOperator(operator, zoneParameters.offer[0].token); + vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "beforeAuthorizedTransfer(address operator, address token)")); + zone.exposed_validateSubstandard7(context, zoneParameters, true); + } + + function test_validateSubstandard7_revertsIfTransferValidatorAfterAuthorizedTransferReverts() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + address operator = makeAddr("operator"); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x07), zoneParameters.consideration[0].identifier, address(transferValidator), operator); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + transferValidator.revertAfterAuthorizedTransfer(zoneParameters.offer[0].token); + vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "afterAuthorizedTransfer(address token)")); + zone.exposed_validateSubstandard7(context, zoneParameters, false); + } + + function test_validateSubstandard7_beforeHookReturns73OnSuccess() public { + _test_validateSubstandard7_returns73OnSuccess(true); + } + + function test_validateSubstandard7_afterHookReturns73OnSuccess() public { + _test_validateSubstandard7_returns73OnSuccess(false); + } + + function _test_validateSubstandard7_returns73OnSuccess(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + address operator = makeAddr("operator"); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x07), zoneParameters.consideration[0].identifier, address(transferValidator), operator); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard7(context, zoneParameters, before); + assertEq(substandardLengthResult, 73); + } + + /* _validateSubstandard8 */ + + function test_validateSubstandard8_returnsZeroLengthIfNotSubstandard8() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes memory context = abi.encodePacked(bytes1(0x04)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard8(context, zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard8_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + bytes memory context = abi.encodePacked(bytes1(0x08), bytes10(0)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 8 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard8(context, zoneParameters, true); + } + + function test_validateSubstandard8_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 0, + amount: 50 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x5), + identifier: 45, + amount: 1, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(46), address(0x6)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert(abi.encodeWithSelector(Substandard8IdentifierViolation.selector, zoneParameters.orderHash, 45, 46)); + zone.exposed_validateSubstandard8(context, zoneParameters, true); + } + + function test_validateSubstandard8_revertsIfItemTypeRequirementsAreNotMet() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 0, + amount: 50 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(0x6)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + vm.expectRevert(abi.encodeWithSelector(Substandard8UnexpectedItemTypeViolation.selector, zoneParameters.orderHash)); + zone.exposed_validateSubstandard8(context, zoneParameters, true); + } + + function test_validateSubstandard8_revertsIfTransferValidatorBeforeAuthorizedTransferReverts() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x08), zoneParameters.consideration[0].identifier, address(transferValidator)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + transferValidator.revertBeforeAuthorizedTransferWithTokenId(address(0x8), zoneParameters.offer[0].identifier); + vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "beforeAuthorizedTransfer(address token, uint256 tokenId)")); + zone.exposed_validateSubstandard8(context, zoneParameters, true); + } + + function test_validateSubstandard8_revertsIfTransferValidatorAfterAuthorizedTransferReverts() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x08), zoneParameters.consideration[0].identifier, address(transferValidator)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + transferValidator.revertAfterAuthorizedTransferWithTokenId(address(0x8), zoneParameters.offer[0].identifier); + vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "afterAuthorizedTransfer(address token, uint256 tokenId)")); + zone.exposed_validateSubstandard8(context, zoneParameters, false); + } + + function test_validateSubstandard8_beforeHookReturns53OnSuccess() public { + _test_validateSubstandard8_returns53OnSuccess(true); + } + + function test_validateSubstandard8_afterHookReturns53OnSuccess() public { + _test_validateSubstandard8_returns53OnSuccess(false); + } + + function _test_validateSubstandard8_returns53OnSuccess(bool before) private { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + MockTransferValidator transferValidator = new MockTransferValidator(); + + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + zoneParameters.offer = spentItems; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + zoneParameters.consideration = receivedItems; + + bytes memory context = abi.encodePacked(bytes1(0x08), zoneParameters.consideration[0].identifier, address(transferValidator)); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + context + ); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard8(context, zoneParameters, before); + assertEq(substandardLengthResult, 53); + } + + /* _deriveReceivedItemsHash */ + + function test_deriveReceivedItemsHash_returnsHashIfNoReceivedItems() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](0); + + bytes32 receivedItemsHash = zone.exposed_deriveReceivedItemsHash(receivedItems, 0, 0); + assertEq(receivedItemsHash, bytes32(0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)); + } + + function test_deriveReceivedItemsHash_returnsHashForValidReceivedItems() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](2); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + receivedItems[1] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 199, + amount: 10, + recipient: payable(address(0x3)) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 100, 10)); + bytes32 receivedItemsHash = zone.exposed_deriveReceivedItemsHash(receivedItems, 100, 10); + assertEq(receivedItemsHash, bytes32(0x8f5c27e415d7805dea8816d4030dc2c0ce11f8f48a0adcde373021dec7b41aad)); + } + + function test_deriveReceivedItemsHash_returnsHashForReceivedItemWithAVeryLargeAmount() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, type(uint256).max, 100)); + bytes32 receivedItemsHash = zone.exposed_deriveReceivedItemsHash(receivedItems, type(uint256).max, 100); + assertEq(receivedItemsHash, bytes32(0xdb99f7eb854f29cd6f8faedea38d7da25073ef9876653ff45ab5c10e51f8ce4f)); + } + + /* _bytes32ArrayIncludes */ + + function test_bytes32ArrayIncludes_returnsFalseIfSourceArrayIsSmallerThanValuesArray() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory sourceArray = new bytes32[](1); + bytes32[] memory valuesArray = new bytes32[](2); + + bool includesEmptySource = zone.exposed_bytes32ArrayIncludes(sourceArray, valuesArray); + assertFalse(includesEmptySource); + } + + function test_bytes32ArrayIncludes_returnsFalseIfSourceArrayDoesNotIncludeValuesArray() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory sourceArray = new bytes32[](2); + sourceArray[0] = bytes32(uint256(1)); + sourceArray[1] = bytes32(uint256(2)); + bytes32[] memory valuesArray = new bytes32[](2); + valuesArray[0] = bytes32(uint256(3)); + valuesArray[1] = bytes32(uint256(4)); + + bool includes = zone.exposed_bytes32ArrayIncludes(sourceArray, valuesArray); + assertFalse(includes); + } + + function test_bytes32ArrayIncludes_returnsTrueIfSourceArrayEqualsValuesArray() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory sourceArray = new bytes32[](2); + sourceArray[0] = bytes32(uint256(1)); + sourceArray[1] = bytes32(uint256(2)); + bytes32[] memory valuesArray = new bytes32[](2); + valuesArray[0] = bytes32(uint256(1)); + valuesArray[1] = bytes32(uint256(2)); + + bool includes = zone.exposed_bytes32ArrayIncludes(sourceArray, valuesArray); + assertTrue(includes); + } + + function test_bytes32ArrayIncludes_returnsTrueIfValuesArrayIsASubsetOfSourceArray() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory sourceArray = new bytes32[](4); + sourceArray[0] = bytes32(uint256(1)); + sourceArray[1] = bytes32(uint256(2)); + sourceArray[2] = bytes32(uint256(3)); + sourceArray[3] = bytes32(uint256(4)); + bytes32[] memory valuesArray = new bytes32[](2); + valuesArray[0] = bytes32(uint256(1)); + valuesArray[1] = bytes32(uint256(2)); + + bool includes = zone.exposed_bytes32ArrayIncludes(sourceArray, valuesArray); + assertTrue(includes); + } + + /* helper functions */ + + function _newZone(address owner) private returns (ImmutableSignedZoneV3) { + return new ImmutableSignedZoneV3( + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", owner + ); + } + + function _newZoneHarness(address owner) private returns (ImmutableSignedZoneV3Harness) { + return new ImmutableSignedZoneV3Harness( + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", owner + ); + } + + function _defaultBaseZoneParameters() private view returns (ZoneParameters memory) { + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x66), identifier: 111, amount: 1}); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({itemType: ItemType.ERC20, token: address(0x77), identifier: 0, amount: 10, recipient: payable(address(0x3))}); + + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = orderHash; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: orderHash, + fulfiller: FULFILLER, + offerer: OFFERER, + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + return zoneParameters; + } + + function _buildExtraData( + ImmutableSignedZoneV3Harness zone, + uint256 signerPrivateKey, + address fulfiller, + uint64 expiration, + bytes32 orderHash, + bytes memory context + ) private view returns (bytes memory) { + bytes32 eip712SignedOrderHash = zone.exposed_deriveSignedOrderHash(fulfiller, expiration, orderHash, context); + bytes memory extraData = abi.encodePacked( + bytes1(0), + fulfiller, + expiration, + _signCompact(signerPrivateKey, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash)), + context + ); + return extraData; + } +} + +// solhint-enable func-name-mixedcase diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol new file mode 100644 index 00000000..351791db --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -0,0 +1,114 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2025 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ReceivedItem, ZoneParameters} from "seaport-types-16/src/lib/ConsiderationStructs.sol"; +import {ImmutableSignedZoneV3} from + "../../../../../../contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { + constructor( + string memory zoneName, + address seaport, + string memory apiEndpoint, + string memory documentationURI, + address owner + ) + ImmutableSignedZoneV3(zoneName, seaport, apiEndpoint, documentationURI, owner) + {} + + function exposed_domainSeparator() external view returns (bytes32) { + return _domainSeparator(); + } + + function exposed_deriveDomainSeparator() external view returns (bytes32 domainSeparator) { + return _deriveDomainSeparator(); + } + + function exposed_getSupportedSubstandards() external pure returns (uint256[] memory substandards) { + return _getSupportedSubstandards(); + } + + function exposed_deriveSignedOrderHash( + address fulfiller, + uint64 expiration, + bytes32 orderHash, + bytes calldata context + ) external pure returns (bytes32 signedOrderHash) { + return _deriveSignedOrderHash(fulfiller, expiration, orderHash, context); + } + + function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + { + return _validateSubstandards(context, zoneParameters, before); + } + + function exposed_validateSubstandard1(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256) + { + return _validateSubstandard1(context, zoneParameters, before); + } + + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256) + { + return _validateSubstandard3(context, zoneParameters, before); + } + + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256) + { + return _validateSubstandard4(context, zoneParameters, before); + } + + function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + pure + returns (uint256) + { + return _validateSubstandard6(context, zoneParameters, before); + } + + function exposed_validateSubstandard7(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + returns (uint256) + { + return _validateSubstandard7(context, zoneParameters, before); + } + + function exposed_validateSubstandard8(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + returns (uint256) + { + return _validateSubstandard8(context, zoneParameters, before); + } + + function exposed_deriveReceivedItemsHash( + ReceivedItem[] calldata receivedItems, + uint256 scalingFactorNumerator, + uint256 scalingFactorDenominator + ) external pure returns (bytes32) { + return _deriveReceivedItemsHash(receivedItems, scalingFactorNumerator, scalingFactorDenominator); + } + + function exposed_bytes32ArrayIncludes(bytes32[] calldata sourceArray, bytes32[] memory values) + external + pure + returns (bool) + { + return _bytes32ArrayIncludes(sourceArray, values); + } +} + +// solhint-enable func-name-mixedcase diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md new file mode 100644 index 00000000..f9cc7de8 --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -0,0 +1,144 @@ +# Test Plan for Immutable Signed Zone (v3) + +## ImmutableSignedZoneV3.sol + +Constructor tests: + +| Test name | Description | Happy Case | Implemented | +| ------------------------------------------------------------- | ------------------------------------------------------------- | ---------- | ----------- | +| `test_contructor_grantsAdminRoleToOwner` | Check `DEFAULT_ADMIN_ROLE` is granted to the specified owner. | Yes | Yes | +| `test_contructor_emitsSeaportCompatibleContractDeployedEvent` | Emits `SeaportCompatibleContractDeployed` event. | Yes | Yes | + +Control function tests: + +| Test name | Description | Happy Case | Implemented | +| ------------------------------------------------------------------------------ | ----------------------------------------------- | ---------- | ----------- | +| `test_grantRole_revertsIfCalledByNonAdminRole` | Grant role without authorization | No | Yes | +| `test_grantRole_grantsIfCalledByAdminRole` | Grant role with authorization | Yes | Yes | +| `test_revokeRole_revertsIfCalledByNonAdminRole` | Revoke role without authorization | No | Yes | +| `test_revokeRole_revertsIfRevokingLastDefaultAdminRole` | Revoke last `DEFAULT_ADMIN_ROLE`. | No | Yes | +| `test_revokeRole_revokesIfRevokingNonLastDefaultAdminRole` | Revoke non-last `DEFAULT_ADMIN_ROLE`. | Yes | Yes | +| `test_revokeRole_revokesIfRevokingLastNonDefaultAdminRole` | Revoke last non-`DEFAULT_ADMIN_ROLE`. | Yes | Yes | +| `test_renounceRole_revertsIfCallerDoesNotMatchCallerConfirmationAddress` | Renounce role without authorization | No | Yes | +| `test_renounceRole_revertsIfRenouncingLastDefaultAdminRole` | Renounce last `DEFAULT_ADMIN_ROLE`. | No | Yes | +| `test_renounceRole_revokesIfRenouncingNonLastDefaultAdminRole` | Renounce non-last `DEFAULT_ADMIN_ROLE`. | Yes | Yes | +| `test_renounceRole_revokesIfRenouncingLastNonDefaultAdminRole` | Renounce last non-`DEFAULT_ADMIN_ROLE`. | Yes | Yes | +| `test_addSigner_revertsIfCalledByNonZoneManagerRole` | Add signer without authorization. | No | Yes | +| `test_addSigner_revertsIfSignerIsTheZeroAddress` | Add zero address as signer. | No | Yes | +| `test_addSigner_emitsSignerAddedEvent` | Emits `SignerAdded` event. | Yes | Yes | +| `test_addSigner_revertsIfSignerAlreadyActive` | Add an already active signer. | No | Yes | +| `test_addSigner_revertsIfSignerWasPreviouslyActive` | Add a previously active signer. | No | Yes | +| `test_removeSigner_revertsIfCalledByNonZoneManagerRole` | Remove signer without authorization. | Yes | Yes | +| `test_removeSigner_revertsIfSignerNotActive` | Remove a signer that is not active. | No | Yes | +| `test_removeSigner_emitsSignerRemovedEvent` | Emits `SignerRemoved` event. | Yes | Yes | +| `test_updateAPIEndpoint_revertsIfCalledByNonZoneManagerRole` | Update API endpoint without authorization. | No | Yes | +| `test_updateAPIEndpoint_updatesAPIEndpointIfCalledByZoneManagerRole` | Update API endpoint with authorization. | Yes | Yes | +| `test_updateDocumentationURI_revertsIfCalledByNonZoneManagerRole` | Update documentation URI without authorization. | No | Yes | +| `test_updateDocumentationURI_updatesDocumentationURIIfCalledByZoneManagerRole` | Update documentation URI with authorization. | Yes | Yes | + +Operational function tests: + +| Test name | Description | Happy Case | Implemented | +| --------------------------------------------------------------------------- | --------------------------------------------------- | ---------- | ----------- | +| `test_getSeaportMetadata` | Retrieve metadata describing the Zone. | Yes | Yes | +| `test_sip7Information` | Retrieve SIP-7 specific information. | Yes | Yes | +| `test_supportsInterface` | ERC165 support. | Yes | Yes | +| `test_authorizeOrder_revertsIfSeaportNotCaller` | `authorizeOrder` caller not Seaport. | No | Yes | +| `test_authorizeOrder_allowedIfSeaportIsCaller` | `authorizeOrder` caller is Seaport. | Yes | Yes | +| `test_authorizeOrder_revertsIfEmptyExtraData` | Authorize order with empty `extraData`. | No | Yes | +| `test_authorizeOrder_revertsIfExtraDataLengthIsLessThan93` | Authorize order with unexpected `extraData` length. | No | Yes | +| `test_authorizeOrder_revertsIfExtraDataVersionIsNotSupported` | Authorize order with unexpected SIP-6 version byte. | No | Yes | +| `test_authorizeOrder_revertsIfSignatureHasExpired` | Authorize order with an expired signature. | No | Yes | +| `test_authorizeOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Authorize order with unexpected fufiller. | No | Yes | +| `test_authorizeOrder_revertsIfNoSpentItems` | Authorize order with no spent items. | No | Yes | +| `test_authorizeOrder_revertsIfNoReceivedItems` | Authorize order with no received items. | No | Yes | +| `test_authorizeOrder_revertsIfSignerIsNotActive` | Authorize order with inactive signer. | No | Yes | +| `test_authorizeOrder_returnsMagicValueOnSuccessfulValidation` | Authorize order successfully. | Yes | Yes | +| `test_validateOrder_revertsIfSeaportNotCaller` | `validateOrder` caller not Seaport. | No | Yes | +| `test_validateOrder_allowedIfSeaportIsCaller` | `validateOrder` caller is Seaport. | Yes | Yes | +| `test_validateOrder_revertsIfContextIsEmpty` | Validate order with an empty context. | No | Yes | +| `test_validateOrder_returnsMagicValueOnSuccessfulValidation` | Validate order successfully. | Yes | Yes | + +Internal operational function tests: + +| Test name | Description | Happy Case | Implemented | +| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ---------- | ----------- | +| `test_domainSeparator_returnsCachedDomainSeparatorWhenChainIDMatchesValueSetOnDeployment` | Domain separator basic test. | Yes | Yes | +| `test_domainSeparator_returnsUpdatedDomainSeparatorIfChainIDIsDifferentFromValueSetOnDeployment` | Domain separator changes when chain ID changes. | Yes | Yes | +| `test_deriveDomainSeparator_returnsDomainSeparatorForChainID` | Domain separator derivation. | Yes | Yes | +| `test_getSupportedSubstandards` | Retrieve Zone's supported substandards. | Yes | Yes | +| `test_deriveSignedOrderHash_returnsHashOfSignedOrder` | Signed order hash derivation. | Yes | Yes | +| `test_validateSubstandards_revertsIfEmptyContext` | Empty context without substandards. | No | Yes | +| `test_validateSubstandards_beforeHookSubstandard1` | Context with substandard 1 in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookSubstandard1` | Context with substandard 1 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandard3` | Context with substandard 3 in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookSubstandard3` | Context with substandard 3 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandard4` | Context with substandard 4 in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookSubstandard4` | Context with substandard 4 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandard6` | Context with substandard 6 in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookSubstandard6` | Context with substandard 6 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandard7` | Context with substandard 7 in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookSubstandard7` | Context with substandard 7 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandard8` | Context with substandard 8 in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookSubstandard8` | Context with substandard 8 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookMultipleSubstandardsInCorrectOrder` | Context with multiple substandards in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookMultipleSubstandardsInCorrectOrder` | Context with multiple substandards in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandards3Then6` | Context with substandards 3 and 6, but not 4 in before hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookSubstandards3Then6` | Context with substandards 3 and 6, but not 4 in after hook. | Yes | Yes | +| `test_validateSubstandards_beforeHookManySubstandards` | Context with many substandards in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookManySubstandards` | Context with many substandards in after hook. | Yes | Yes | +| `test_validateSubstandards_revertsOnMultipleSubstandardsInIncorrectOrder` | Context with multiple substandards out of order. | No | Yes | +| `test_validateSubstandard1_returnsZeroLengthIfNotSubstandard1` | Substandard 1 validation skips when version byte is not 1. | Yes | Yes | +| `test_validateSubstandard1_revertsIfContextLengthIsInvalid` | Substandard 1 validation with invalid data. | No | Yes | +| `test_validateSubstandard1_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext` | Substandard 1 validation when first received item identifier doesn't match expected identifier. | No | Yes | +| `test_validateSubstandard1_beforeHookReturns33OnSuccess` | Substandard 1 validation when first received item identifier matches expected identifier. | Yes | Yes | +| `test_validateSubstandard1_afterHookReturns33OnSuccess` | Substandard 1 success in after hook. | Yes | Yes | +| `test_validateSubstandard3_returnsZeroLengthIfNotSubstandard3` | Substandard 3 validation skips when version byte is not 3. | Yes | Yes | +| `test_validateSubstandard3_revertsIfContextLengthIsInvalid` | Substandard 3 validation with invalid data. | No | Yes | +| `test_validateSubstandard3_revertsIfDerivedReceivedItemsHashNotEqualToHashInContext` | Substandard 3 validation when derived hash doesn't match expected hash. | No | Yes | +| `test_validateSubstandard3_beforeHookReturns33OnSuccess` | Substandard 3 validation when derived hash matches expected hash. | Yes | Yes | +| `test_validateSubstandard3_afterHookReturns33OnSuccess` | Substandard 3 success in after hook. | Yes | Yes | +| `test_validateSubstandard4_returnsZeroLengthIfNotSubstandard4` | Substandard 4 validation skips when version byte is not 4. | Yes | Yes | +| `test_validateSubstandard4_revertsIfContextLengthIsInvalid` | Substandard 4 validation with invalid data. | No | Yes | +| `test_validateSubstandard4_revertsIfExpectedOrderHashesAreNotPresent` | Substandard 4 validation when required order hashes are not present. | No | Yes | +| `test_validateSubstandard4_beforeHookReturnsLengthOfSubstandardSegmentOnSuccess` | Substandard 4 success in before hook. | Yes | Yes | +| `test_validateSubstandard4_afterHookReturnsLengthOfSubstandardSegmentOnSuccess` | Substandard 4 validation when required order hashes are present. | Yes | Yes | +| `test_validateSubstandard6_returnsZeroLengthIfNotSubstandard6` | Substandard 6 validation skips when version byte is not 6. | Yes | Yes | +| `test_validateSubstandard6_revertsIfContextLengthIsInvalid` | Substandard 6 validation with invalid data. | No | Yes | +| `test_validateSubstandard6_revertsIfDerivedReceivedItemsHashesIsNotEqualToHashesInContext` | Substandard 6 validation when derived hash doesn't match expected hash. | No | Yes | +| `test_validateSubstandard6_beforeHookReturnsLengthOfSubstandardSegmentOnSuccess` | Substandard 6 validation when derived hash matches expected hash. | Yes | Yes | +| `test_validateSubstandard6_afterHookReturnsLengthOfSubstandardSegmentOnSuccess` | Substandard 6 success in after hook. | Yes | Yes | +| `test_validateSubstandard7_returnsZeroLengthIfNotSubstandard7` | Substandard 7 validation skips when version byte is not 7. | Yes | Yes | +| `test_validateSubstandard7_revertsIfContextLengthIsInvalid` | Substandard 7 validation with invalid data. | No | Yes | +| `test_validateSubstandard7_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext` | Substandard 7 validation when first received item identifier doesn't match expected identifier. | No | Yes | +| `test_validateSubstandard7_revertsIfItemTypeRequirementsAreNotMet` | Substandard 7 validation when item type requirements are not met. | No | Yes | +| `test_validateSubstandard7_revertsIfTransferValidatorBeforeAuthorizedTransferReverts` | Substandard 7 validation when `ITransferValidator.beforeAuthorizedTransfer` reverts | No | Yes | +| `test_validateSubstandard7_revertsIfTransferValidatorAfterAuthorizedTransferReverts` | Substandard 7 validation when `ITransferValidator.afterAuthorizedTransfer` reverts | No | Yes | +| `test_validateSubstandard7_beforeHookReturns73OnSuccess` | Substandard 7 validations are successful in before hook. | Yes | Yes | +| `test_validateSubstandard7_afterHookReturns73OnSuccess` | Substandard 7 validations are successful in after hook. | Yes | Yes | +| `test_validateSubstandard8_returnsZeroLengthIfNotSubstandard8` | Substandard 8 validation skips when version byte is not 8. | Yes | Yes | +| `test_validateSubstandard8_revertsIfContextLengthIsInvalid` | Substandard 8 validation with invalid data. | No | Yes | +| `test_validateSubstandard8_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext` | Substandard 8 validation when first received item identifier doesn't match expected identifier. | No | Yes | +| `test_validateSubstandard8_revertsIfItemTypeRequirementsAreNotMet` | Substandard 8 validation when item type requirements are not met. | No | Yes | +| `test_validateSubstandard8_revertsIfTransferValidatorBeforeAuthorizedTransferReverts` | Substandard 8 validation when `ITransferValidator.beforeAuthorizedTransfer` reverts | No | Yes | +| `test_validateSubstandard8_revertsIfTransferValidatorAfterAuthorizedTransferReverts` | Substandard 8 validation when `ITransferValidator.afterAuthorizedTransfer` reverts | No | Yes | +| `test_validateSubstandard8_beforeHookReturns53OnSuccess` | Substandard 8 validations are successful in before hook. | Yes | Yes | +| `test_validateSubstandard8_afterHookReturns53OnSuccess` | Substandard 8 validations are successful in after hook. | Yes | Yes | +| `test_deriveReceivedItemsHash_returnsHashIfNoReceivedItems` | Received items derivation with not items. | Yes | Yes | +| `test_deriveReceivedItemsHash_returnsHashForValidReceivedItems` | Received items derivation with some items. | Yes | Yes | +| `test_deriveReceivedItemsHash_returnsHashForReceivedItemWithAVeryLargeAmount` | Received items derivation with scaling factor forcing `> uint256` intermediate calcualtions. | Yes | Yes | +| `test_bytes32ArrayIncludes_returnsFalseIfSourceArrayIsSmallerThanValuesArray` | `byte32` array inclusion check when more values than in source. | Yes | Yes | +| `test_bytes32ArrayIncludes_returnsFalseIfSourceArrayDoesNotIncludeValuesArray` | `byte32` array inclusion check when values are not present in source. | Yes | Yes | +| `test_bytes32ArrayIncludes_returnsTrueIfSourceArrayEqualsValuesArray` | `byte32` array inclusion check when source and values are identical. | Yes | Yes | +| `test_bytes32ArrayIncludes_returnsTrueIfValuesArrayIsASubsetOfSourceArray` | `byte32` array inclusion check when values are present in source. | Yes | Yes | + +Integration tests: + +All of these tests are in [test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol](../../../ImmutableSeaportSignedZoneV3Integration.t.sol). + +| Test name | Description | Happy Case | Implemented | +| ---------------------------------------------------- | ------------------------------- | ---------- | ----------- | +| `test_fulfillAdvancedOrder_withCompleteFulfilment` | Full fulfilment. | Yes | Yes | +| `test_fulfillAdvancedOrder_withPartialFill` | Partial fulfilment. | Yes | Yes | +| `test_fulfillAdvancedOrder_withMultiplePartialFills` | Sequential partial fulfilments. | Yes | Yes | +| `test_fulfillAdvancedOrder_withOverfilling` | Over fulfilment. | Yes | Yes | diff --git a/yarn.lock b/yarn.lock index d295a520..037fd4af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -385,6 +385,11 @@ resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== +"@ethereumjs/rlp@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842" + integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA== + "@ethereumjs/tx@3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.2.tgz#348d4624bf248aaab6c44fec2ae67265efe3db00" @@ -418,6 +423,14 @@ ethereum-cryptography "^2.0.0" micro-ftch "^0.3.1" +"@ethereumjs/util@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-9.1.0.tgz#75e3898a3116d21c135fa9e29886565609129bce" + integrity sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog== + dependencies: + "@ethereumjs/rlp" "^5.0.2" + ethereum-cryptography "^2.2.1" + "@ethereumjs/vm@5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.6.0.tgz#e0ca62af07de820143674c30b776b86c1983a464" @@ -894,6 +907,22 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@limitbreak/creator-token-standards@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@limitbreak/creator-token-standards/-/creator-token-standards-5.0.0.tgz#35f6c8929351e765d1f4d85974e61e5516ed9db8" + integrity sha512-BhrD3SMCq8YrGbJildBbu4BpHia7uby60XpGYVyFnb4xEvFey7bRbnOeVQ2mrTx07y02KvTWS5gESIPj5O4Mtg== + dependencies: + "@limitbreak/permit-c" "1.0.0" + "@openzeppelin/contracts" "4.8.3" + erc721a "4.2.3" + +"@limitbreak/permit-c@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@limitbreak/permit-c/-/permit-c-1.0.0.tgz#96df6527ef2562ac1e8bd363d040419df465abca" + integrity sha512-7BooxTklXlCPzfdccfKL7Tt2Cm4MntOHR51dHqjKePn7AynMKsUtaKH75ZXHzWRPZSmyixFNzQ7tIJDdPxF2MA== + dependencies: + "@openzeppelin/contracts" "4.8.3" + "@metamask/eth-sig-util@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" @@ -919,6 +948,20 @@ dependencies: "@noble/hashes" "1.3.3" +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/curves@~1.8.1": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.2.tgz#8f24c037795e22b90ae29e222a856294c1d9ffc7" + integrity sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g== + dependencies: + "@noble/hashes" "1.7.2" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -934,6 +977,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/hashes@1.4.0", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/hashes@1.7.2", "@noble/hashes@~1.7.1": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" + integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== + "@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -960,6 +1013,54 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/edr-darwin-arm64@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.3.tgz#d8e2609fc24cf20e75c3782e39cd5a95f7488075" + integrity sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA== + +"@nomicfoundation/edr-darwin-x64@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.3.tgz#7a9e94cee330269a33c7f1dce267560c7e12dbd3" + integrity sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA== + +"@nomicfoundation/edr-linux-arm64-gnu@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.3.tgz#cd5ec90c7263045c3dfd0b109c73206e488edc27" + integrity sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ== + +"@nomicfoundation/edr-linux-arm64-musl@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.3.tgz#ed23df2d9844470f5661716da27d99a72a69e99e" + integrity sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA== + +"@nomicfoundation/edr-linux-x64-gnu@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.3.tgz#87a62496c2c4b808bc4a9ae96cca1642a21c2b51" + integrity sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw== + +"@nomicfoundation/edr-linux-x64-musl@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.3.tgz#8cfe408c73bcb9ed5e263910c313866d442f4b48" + integrity sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg== + +"@nomicfoundation/edr-win32-x64-msvc@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.3.tgz#fb208b94553c7eb22246d73a1ac4de5bfdb97d01" + integrity sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung== + +"@nomicfoundation/edr@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.11.3.tgz#e8b30b868788e45d7a2ee2359a021ef7dcb96952" + integrity sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g== + dependencies: + "@nomicfoundation/edr-darwin-arm64" "0.11.3" + "@nomicfoundation/edr-darwin-x64" "0.11.3" + "@nomicfoundation/edr-linux-arm64-gnu" "0.11.3" + "@nomicfoundation/edr-linux-arm64-musl" "0.11.3" + "@nomicfoundation/edr-linux-x64-gnu" "0.11.3" + "@nomicfoundation/edr-linux-x64-musl" "0.11.3" + "@nomicfoundation/edr-win32-x64-msvc" "0.11.3" + "@nomicfoundation/ethereumjs-block@5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.2.tgz#13a7968f5964f1697da941281b7f7943b0465d04" @@ -1094,6 +1195,13 @@ mcl-wasm "^0.7.1" rustbn.js "~0.2.0" +"@nomicfoundation/hardhat-foundry@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.2.0.tgz#00bac127d1540c5c3900709f9f5fa511c599ba6c" + integrity sha512-2AJQLcWnUk/iQqHDVnyOadASKFQKF1PhNtt1cONEQqzUPK+fqME1IbP+EKu+RkZTRcyc4xqUMaB0sutglKRITg== + dependencies: + picocolors "^1.1.0" + "@nomicfoundation/hardhat-network-helpers@^1.0.7": version "1.0.10" resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.10.tgz#c61042ceb104fdd6c10017859fdef6529c1d6585" @@ -1208,16 +1316,26 @@ find-up "^4.1.0" fs-extra "^8.1.0" -"@openzeppelin/contracts-upgradeable@^4.9.3", "openzeppelin-contracts-upgradeable-4.9.3@npm:@openzeppelin/contracts-upgradeable@^4.9.3": +"@openzeppelin/contracts-upgradeable@^4.9.3": version "4.9.5" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.5.tgz#572b5da102fc9be1d73f34968e0ca56765969812" integrity sha512-f7L1//4sLlflAN7fVzJLoRedrf5Na3Oal5PZfIq55NFcVZ90EpV1q5xOvL4lFvg3MNICSDr2hH0JUBxwlxcoPg== +"@openzeppelin/contracts@4.8.3": + version "4.8.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.3.tgz#cbef3146bfc570849405f59cba18235da95a252a" + integrity sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg== + "@openzeppelin/contracts@^4.9.2", "@openzeppelin/contracts@^4.9.3": version "4.9.5" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.5.tgz#1eed23d4844c861a1835b5d33507c1017fa98de8" integrity sha512-ZK+W5mVhRppff9BE6YdR8CC52C8zAvsVAiWhEtQ5+oNxFE6h1WdeWo+FJSF8KKvtxxVYZ7MTP/5KoVpAU3aSWg== +"@openzeppelin/contracts@^4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677" + integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA== + "@openzeppelin/test-helpers@^0.5.16": version "0.5.16" resolved "https://registry.yarnpkg.com/@openzeppelin/test-helpers/-/test-helpers-0.5.16.tgz#2c9054f85069dfbfb5e8cef3ed781e8caf241fb3" @@ -1322,6 +1440,16 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== +"@scure/base@~1.1.6": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + +"@scure/base@~1.2.5": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" + integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== + "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -1349,6 +1477,15 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" +"@scure/bip32@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" + integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -1373,6 +1510,14 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" +"@scure/bip39@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" + integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== + dependencies: + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -2999,6 +3144,13 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3747,6 +3899,11 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +erc721a@4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/erc721a/-/erc721a-4.2.3.tgz#ca6469b0e54afb0f614272c2147dc4cb49ff223f" + integrity sha512-0deF0hOOK1XI1Vxv3NKDh2E9sgzRlENuOoexjXRJIRfYCsLlqi9ejl2RF6Wcd9HfH0ldqC03wleQ2WDjxoOUvA== + errno@~0.1.1: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -4287,6 +4444,16 @@ ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: "@scure/bip32" "1.3.3" "@scure/bip39" "1.2.2" +ethereum-cryptography@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz#58f2810f8e020aecb97de8c8c76147600b0b8ccf" + integrity sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg== + dependencies: + "@noble/curves" "1.4.2" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + ethereum-waffle@^4.0.10: version "4.0.10" resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-4.0.10.tgz#f1ef1564c0155236f1a66c6eae362a5d67c9f64c" @@ -4555,6 +4722,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -5199,7 +5371,7 @@ hardhat-gas-reporter@^1.0.9: eth-gas-reporter "^0.2.25" sha1 "^1.1.1" -hardhat@^2.12.7, hardhat@^2.17.3: +hardhat@^2.17.3: version "2.19.5" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.19.5.tgz#6017c35ae2844b669e9bcc84c3d05346d4ef031c" integrity sha512-vx8R7zWCYVgM56vA6o0Wqx2bIIptkN4TMs9QwDqZVNGRhMzBfzqUeEYbp+69gxWp1neg2V2nYQUaaUv7aom1kw== @@ -5254,6 +5426,51 @@ hardhat@^2.12.7, hardhat@^2.17.3: uuid "^8.3.2" ws "^7.4.6" +hardhat@^2.21.0, hardhat@^2.26.5: + version "2.26.5" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.26.5.tgz#831073e3bc9d034fbb997078aa819f38538d2d7f" + integrity sha512-TvFKUPGRaoemeVpnKsXt5I+kVCNrzP2cLwyNUveu0JKf2Q0lzh6LTgVBsWyYPlXAwBzyUQ6fsL98UgyF/QdOfA== + dependencies: + "@ethereumjs/util" "^9.1.0" + "@ethersproject/abi" "^5.1.2" + "@nomicfoundation/edr" "^0.11.3" + "@nomicfoundation/solidity-analyzer" "^0.1.0" + "@sentry/node" "^5.18.1" + adm-zip "^0.4.16" + aggregate-error "^3.0.0" + ansi-escapes "^4.3.0" + boxen "^5.1.2" + chokidar "^4.0.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + ethereum-cryptography "^1.0.3" + find-up "^5.0.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + json-stream-stringify "^3.1.4" + keccak "^3.0.2" + lodash "^4.17.11" + micro-eth-signer "^0.14.0" + mnemonist "^0.38.0" + mocha "^10.0.0" + p-map "^4.0.0" + picocolors "^1.1.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + solc "0.8.26" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + tinyglobby "^0.2.6" + tsort "0.0.1" + undici "^5.14.0" + uuid "^8.3.2" + ws "^7.4.6" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -5928,6 +6145,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stream-stringify@^3.1.4: + version "3.1.6" + resolved "https://registry.yarnpkg.com/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz#ebe32193876fb99d4ec9f612389a8d8e2b5d54d4" + integrity sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog== + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -6410,7 +6632,7 @@ merkle-patricia-tree@^4.2.2, merkle-patricia-tree@^4.2.4: readable-stream "^3.6.0" semaphore-async-await "^1.5.1" -merkletreejs@^0.3.11: +merkletreejs@^0.3.11, merkletreejs@^0.3.9: version "0.3.11" resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.3.11.tgz#e0de05c3ca1fd368de05a12cb8efb954ef6fc04f" integrity sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ== @@ -6426,11 +6648,27 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micro-eth-signer@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz#8aa1fe997d98d6bdf42f2071cef7eb01a66ecb22" + integrity sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw== + dependencies: + "@noble/curves" "~1.8.1" + "@noble/hashes" "~1.7.1" + micro-packed "~0.7.2" + micro-ftch@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== +micro-packed@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/micro-packed/-/micro-packed-0.7.3.tgz#59e96b139dffeda22705c7a041476f24cabb12b6" + integrity sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg== + dependencies: + "@scure/base" "~1.2.5" + micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -6958,6 +7196,11 @@ onetime@^6.0.0: resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.2.tgz#b1d03075e49290d06570b2fd42154d76c2a5d210" integrity sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA== +"openzeppelin-contracts-upgradeable-4.9.3@npm:@openzeppelin/contracts-upgradeable@^4.9.3": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.5.tgz#572b5da102fc9be1d73f34968e0ca56765969812" + integrity sha512-f7L1//4sLlflAN7fVzJLoRedrf5Na3Oal5PZfIq55NFcVZ90EpV1q5xOvL4lFvg3MNICSDr2hH0JUBxwlxcoPg== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -7307,11 +7550,21 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7577,6 +7830,11 @@ readable-stream@^3.1.0, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -7895,6 +8153,34 @@ scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1: resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== +"seaport-16@https://github.com/immutable/seaport.git#1.6.0+im4": + version "1.6.0" + resolved "https://github.com/immutable/seaport.git#e058101dbe69b403352598ed989c3afd845e9793" + dependencies: + "@nomicfoundation/hardhat-network-helpers" "^1.0.7" + "@openzeppelin/contracts" "^4.9.6" + ethers "^5.5.3" + ethers-eip712 "^0.2.0" + hardhat "^2.21.0" + merkletreejs "^0.3.9" + seaport-core "1.6.5" + seaport-sol "1.6.0" + seaport-types "1.6.3" + solady "^0.0.84" + +"seaport-core-16@https://github.com/immutable/seaport-core.git#1.6.0+im2": + version "1.6.6" + resolved "https://github.com/immutable/seaport-core.git#9ad91d82609e937a9ba3ef330df396b05f384e44" + dependencies: + seaport-types "1.6.3" + +seaport-core@1.6.5: + version "1.6.5" + resolved "https://registry.yarnpkg.com/seaport-core/-/seaport-core-1.6.5.tgz#97c85dd5161e57ec28df6c43c93ee3eb9943ec66" + integrity sha512-jpGOpaKpH1B49oOYqAYAAVXN8eGlI/NjE6fYHPYlQaDVx325NS5dpiDDgGLtQZNgQ3EbqrfhfB5KyIbg7owyFg== + dependencies: + seaport-types "1.6.3" + seaport-core@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/seaport-core/-/seaport-core-0.0.1.tgz#99db0b605d0fbbfd43ca7a4724e64374ce47f6d4" @@ -7908,6 +8194,14 @@ seaport-core@immutable/seaport-core#1.5.0+im.1: dependencies: seaport-types "^0.0.1" +seaport-sol@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/seaport-sol/-/seaport-sol-1.6.0.tgz#2a71ae8da5af9aecffee21767632ef37aaaaee13" + integrity sha512-a1FBK1jIeEQXZ9CmQvtmfG0w7CE8nIad89btGg7qrrrtF4j1S0Ilmzpe2Hderap05Uvf3EWS9P/aghDQCNAwkA== + dependencies: + seaport-core "^0.0.1" + seaport-types "^0.0.1" + seaport-sol@^1.5.0: version "1.5.3" resolved "https://registry.yarnpkg.com/seaport-sol/-/seaport-sol-1.5.3.tgz#ccb0047bcefb7d29bcd379faddf3a5a9902d0c3a" @@ -7916,6 +8210,16 @@ seaport-sol@^1.5.0: seaport-core "^0.0.1" seaport-types "^0.0.1" +"seaport-types-16@npm:seaport-types@1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/seaport-types/-/seaport-types-1.6.3.tgz#b9993864517d4f9ecccc6b6daf6ef3f52a114e58" + integrity sha512-Rm9dTTEUKmXqMgc5TiRtfX/sFOX6SjKkT9l/spTdRknplYh5tmJ0fMJzbE60pCzV1/Izq0cCua6uvWszo6zOAQ== + +seaport-types@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/seaport-types/-/seaport-types-1.6.3.tgz#b9993864517d4f9ecccc6b6daf6ef3f52a114e58" + integrity sha512-Rm9dTTEUKmXqMgc5TiRtfX/sFOX6SjKkT9l/spTdRknplYh5tmJ0fMJzbE60pCzV1/Izq0cCua6uvWszo6zOAQ== + seaport-types@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/seaport-types/-/seaport-types-0.0.1.tgz#e2a32fe8641853d7dadb1b0232d911d88ccc3f1a" @@ -8220,6 +8524,19 @@ solc@0.8.15: semver "^5.5.0" tmp "0.0.33" +solc@0.8.26: + version "0.8.26" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" + integrity sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g== + dependencies: + command-exists "^1.2.8" + commander "^8.1.0" + follow-redirects "^1.12.1" + js-sha3 "0.8.0" + memorystream "^0.3.1" + semver "^5.5.0" + tmp "0.0.33" + solc@^0.4.20: version "0.4.26" resolved "https://registry.yarnpkg.com/solc/-/solc-0.4.26.tgz#5390a62a99f40806b86258c737c1cf653cc35cb5" @@ -8429,7 +8746,7 @@ string-format@^2.0.0: resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8455,6 +8772,15 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -8505,7 +8831,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8526,6 +8852,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -8720,6 +9053,14 @@ timed-out@^4.0.1: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA== +tinyglobby@^0.2.6: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + title-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" @@ -9753,7 +10094,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9770,6 +10111,15 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"