From 7a05e17ee291d73282ae78bff422182771255b6d Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Fri, 1 Aug 2025 13:38:11 +1000 Subject: [PATCH 01/45] Add support for seaport 1.6. --- .gitmodules | 7 + DEPS.md | 21 + .../trading/seaport16/ImmutableSeaport.sol | 633 +++++++++++++++++ .../seaport16/conduit/ConduitController.sol | 5 + .../interfaces/ImmutableSeaportEvents.sol | 15 + .../validators/ReadOnlyOrderValidator.sol | 5 + .../seaport16/validators/SeaportValidator.sol | 5 + .../validators/SeaportValidatorHelper.sol | 5 + .../v3/ImmutableSignedZoneV3.sol | 635 ++++++++++++++++++ .../zones/immutable-signed-zone/v3/README.md | 64 ++ .../v3/ZoneAccessControl.sol | 53 ++ .../v3/interfaces/SIP5EventsAndErrors.sol | 16 + .../v3/interfaces/SIP5Interface.sol | 25 + .../v3/interfaces/SIP6EventsAndErrors.sol | 18 + .../v3/interfaces/SIP6Interface.sol | 14 + .../v3/interfaces/SIP7EventsAndErrors.sol | 86 +++ .../v3/interfaces/SIP7Interface.sol | 74 ++ .../ZoneAccessControlEventsAndErrors.sol | 16 + lib/immutable-seaport-1.6.0+im3 | 1 + lib/immutable-seaport-core-1.6.0+im2 | 1 + remappings.txt | 7 +- 21 files changed, 1705 insertions(+), 1 deletion(-) create mode 100644 DEPS.md create mode 100644 contracts/trading/seaport16/ImmutableSeaport.sol create mode 100644 contracts/trading/seaport16/conduit/ConduitController.sol create mode 100644 contracts/trading/seaport16/interfaces/ImmutableSeaportEvents.sol create mode 100644 contracts/trading/seaport16/validators/ReadOnlyOrderValidator.sol create mode 100644 contracts/trading/seaport16/validators/SeaportValidator.sol create mode 100644 contracts/trading/seaport16/validators/SeaportValidatorHelper.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5EventsAndErrors.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP5Interface.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6EventsAndErrors.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP6Interface.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7Interface.sol create mode 100644 contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/ZoneAccessControlEventsAndErrors.sol create mode 160000 lib/immutable-seaport-1.6.0+im3 create mode 160000 lib/immutable-seaport-core-1.6.0+im2 diff --git a/.gitmodules b/.gitmodules index d11ef0bb..57a35a3b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,10 @@ [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+im3"] + path = lib/immutable-seaport-1.6.0+im3 + url = https://github.com/immutable/seaport 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..8ba83d64 --- /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.17; + +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 ImmutableSeaport16 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..83ed6daa --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -0,0 +1,635 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity 0.8.20; + +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-16/contracts/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 ImmutableSignedZone32 is a zone implementation based on the + * SIP-7 standard https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md + * implementing substandards 3, 4 and 6. + * + * 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("2.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 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 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, + 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 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 Validates a fulfilment execution. + * + * @dev This function is called by Seaport whenever any extraData is + * provided by the caller. + * + * @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 view override returns (bytes4 validOrderMagicValue) { + // 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-21: expected fulfiller. + // (zero address means not restricted). + address expectedFulfiller = address(bytes20(extraData[1:21])); + + // extraData bytes 21-29: expiration timestamp. + uint64 expiration = uint64(bytes8(extraData[21:29])); + + // extraData bytes 29-93: 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); + } + + // Validate supported substandards. + _validateSubstandards(context, zoneParameters); + + // 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); + } + + // All 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 3, 4 and 6 + substandards = new uint256[](3); + substandards[0] = 3; + substandards[1] = 4; + substandards[2] = 6; + } + + /** + * @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 3, 4 and 6 based on context. + * + * @param context Bytes payload of context. + * @param zoneParameters The zone parameters. + */ + function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) internal pure { + uint256 startIndex = 0; + uint256 contextLength = context.length; + + // The ImmutableSignedZoneV2 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 = _validateSubstandard3(context[startIndex:], zoneParameters) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard4(context[startIndex:], zoneParameters) + startIndex; + + if (startIndex == contextLength) return; + startIndex = _validateSubstandard6(context[startIndex:], zoneParameters) + startIndex; + + if (startIndex != contextLength) { + revert InvalidExtraData("invalid context, unexpected context length", zoneParameters.orderHash); + } + } + + /** + * @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. + * @return Length of substandard segment. + */ + function _validateSubstandard3( + bytes calldata context, + ZoneParameters calldata zoneParameters + ) internal pure returns (uint256) { + if (uint8(context[0]) != 3) { + return 0; + } + + if (context.length < 33) { + revert InvalidExtraData("invalid substandard 3 data length", zoneParameters.orderHash); + } + + 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. + * @return Length of substandard segment. + */ + function _validateSubstandard4( + bytes calldata context, + ZoneParameters calldata zoneParameters + ) 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); + 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. + * @return Length of substandard segment. + */ + function _validateSubstandard6( + bytes calldata context, + ZoneParameters calldata zoneParameters + ) internal pure returns (uint256) { + if (uint8(context[0]) != 6) { + return 0; + } + + if (context.length < 65) { + revert InvalidExtraData("invalid substandard 6 data length", zoneParameters.orderHash); + } + + // 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 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..59f9301a --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -0,0 +1,64 @@ +# Immutable Signed Zone (v2) + +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) 3, 4 and 6. + +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 -- 3a. transferFrom --> erc20[IERC20.sol] + seaport -- 3b. transferFrom --> erc721[IERC721.sol] + seaport -- 3c. safeTransferFrom --> erc1155[IERC1155.sol] + seaport -- 4. validateOrder --> Zone + subgraph Zone + direction TB + zone[ImmutableSignedZoneV2.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 `validateOrder` on `ImmutableSignedZoneV2.sol`, passing it the fulfilment execution details as well as the `extraData` parameter +5. `ImmutableSignedZoneV2.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met + +## Differences compared to ImmutableSignedZone (v1) + +The contract was developed based on ImmutableSignedZone, with the addition of: + - SIP7 substandard 6 support + - Role based access control to be role based + +### ZoneAccessControl + +The contract now uses a finer grained access control with role based access with the `ZoneAccessControl` interface, rather than the `Ownable` interface in the v1 contract. A separate `zoneManager` roles is used to manage signers and an admin role used to control roles. + +### Support of SIP7 substandard 6 + +The V2 contract now supports substandard-6 of the SIP7 specification, found here (https://github.com/immutable/platform-services/pull/12775). A server side signed order can adhere to substandard 3 + 4 (full fulfillment only) or substandard 6 + 4 (full or partial fulfillment). 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..34f2b7af --- /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..df579b25 --- /dev/null +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -0,0 +1,86 @@ +// 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 a substandard validation fails. + * This is a custom error that is not part of the SIP-7 spec. + */ + error SubstandardViolation(uint256 substandardId, string reason, bytes32 orderHash); + + /** + * @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); +} 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/lib/immutable-seaport-1.6.0+im3 b/lib/immutable-seaport-1.6.0+im3 new file mode 160000 index 00000000..c72d1cdb --- /dev/null +++ b/lib/immutable-seaport-1.6.0+im3 @@ -0,0 +1 @@ +Subproject commit c72d1cdb77f55b3a377d2279d7bfa45484f07ffe 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/remappings.txt b/remappings.txt index 5d8f7c53..3b0c32fb 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+im3/contracts/ +seaport-core-16/=lib/immutable-seaport-core-1.6.0+im2/ +seaport-sol-16/=lib/immutable-seaport-1.6.0+im3/lib/seaport-sol/ +seaport-types-16/=lib/immutable-seaport-1.6.0+im3/lib/seaport-types/ + From 7a723481228bc9f4889d5e1259474f69fa81b548 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Fri, 1 Aug 2025 13:54:21 +1000 Subject: [PATCH 02/45] Fix imports --- .gitmodules | 5 +++-- lib/immutable-seaport-1.6.0+im3 | 1 - lib/immutable-seaport-1.6.0+im4 | 1 + remappings.txt | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) delete mode 160000 lib/immutable-seaport-1.6.0+im3 create mode 160000 lib/immutable-seaport-1.6.0+im4 diff --git a/.gitmodules b/.gitmodules index 57a35a3b..1ab05f02 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,6 +29,7 @@ [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+im3"] - path = lib/immutable-seaport-1.6.0+im3 + +[submodule "lib/immutable-seaport-1.6.0+im4"] + path = lib/immutable-seaport-1.6.0+im4 url = https://github.com/immutable/seaport diff --git a/lib/immutable-seaport-1.6.0+im3 b/lib/immutable-seaport-1.6.0+im3 deleted file mode 160000 index c72d1cdb..00000000 --- a/lib/immutable-seaport-1.6.0+im3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c72d1cdb77f55b3a377d2279d7bfa45484f07ffe 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/remappings.txt b/remappings.txt index 3b0c32fb..c3fd9c9c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,8 +8,8 @@ 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 -seaport-16/contracts/=lib/immutable-seaport-1.6.0+im3/contracts/ +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+im3/lib/seaport-sol/ -seaport-types-16/=lib/immutable-seaport-1.6.0+im3/lib/seaport-types/ +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/ From 3b1b0cca0163c770269c60b4d46789f7c74e78f5 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Tue, 28 Oct 2025 09:24:35 +1000 Subject: [PATCH 03/45] Seaport 1.6 support --- .../trading/seaport16/ImmutableSeaport.sol | 2 +- .../v3/ImmutableSignedZoneV3.sol | 59 +- .../v3/ZoneAccessControl.sol | 2 +- script/staking/StakeHolderScriptWIMX.t.sol | 30 +- .../seaport/ImmutableSeaportOperational.t.sol | 4 +- .../seaport16/ImmutableSeaportBase.t.sol | 77 + .../seaport16/ImmutableSeaportConfig.t.sol | 17 + .../seaport16/ImmutableSeaportHarness.t.sol | 27 + .../ImmutableSeaportOperational.t.sol | 230 +++ ...utableSeaportSignedZoneV3Integration.t.sol | 901 ++++++++++ .../ImmutableSeaportTestHelper.t.sol | 262 +++ test/trading/seaport16/README.md | 10 + .../v3/IImmutableSignedZoneV3Harness.t.sol | 64 + .../v3/ImmutableSignedZoneV3.t.sol | 1519 +++++++++++++++++ .../v3/ImmutableSignedZoneV3Harness.t.sol | 87 + .../zones/immutable-signed-zone/v3/README.md | 102 ++ 16 files changed, 3360 insertions(+), 33 deletions(-) create mode 100644 test/trading/seaport16/ImmutableSeaportBase.t.sol create mode 100644 test/trading/seaport16/ImmutableSeaportConfig.t.sol create mode 100644 test/trading/seaport16/ImmutableSeaportHarness.t.sol create mode 100644 test/trading/seaport16/ImmutableSeaportOperational.t.sol create mode 100644 test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol create mode 100644 test/trading/seaport16/ImmutableSeaportTestHelper.t.sol create mode 100644 test/trading/seaport16/README.md create mode 100644 test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol create mode 100644 test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol create mode 100644 test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol create mode 100644 test/trading/seaport16/zones/immutable-signed-zone/v3/README.md diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index 8ba83d64..c2d0841e 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -19,7 +19,7 @@ import {ImmutableSeaportEvents} from "./interfaces/ImmutableSeaportEvents.sol"; * spent (the "offer") along with an arbitrary number of items that must * be received back by the indicated recipients (the "consideration"). */ -contract ImmutableSeaport16 is Consideration, Ownable, ImmutableSeaportEvents { +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 diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 83ed6daa..b20462be 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -2,14 +2,14 @@ // SPDX-License-Identifier: Apache-2 // solhint-disable-next-line compiler-version -pragma solidity 0.8.20; +pragma solidity ^0.8.20; 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-16/contracts/interfaces/ZoneInterface.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"; @@ -49,7 +49,7 @@ contract ImmutableSignedZoneV3 is ); /// @dev The EIP-712 domain version value. - bytes32 private constant _VERSION_HASH = keccak256(bytes("2.0")); + bytes32 private constant _VERSION_HASH = keccak256(bytes("3.0")); /// @dev The EIP-712 signed order type hash. bytes32 private constant _SIGNED_ORDER_TYPEHASH = @@ -244,19 +244,19 @@ contract ImmutableSignedZoneV3 is } /** - * @notice Validates a fulfilment execution. + * @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. * * @param zoneParameters The zone parameters containing data related to * the fulfilment execution. - * @return validOrderMagicValue A magic value indicating if the order is - * currently valid. + * @return authorizedOrderMagicValue A magic value indicating if the order + * is currently valid. */ - function validateOrder( + function authorizeOrder( ZoneParameters calldata zoneParameters - ) external view override returns (bytes4 validOrderMagicValue) { + ) external override /* TODO view */ returns (bytes4 authorizedOrderMagicValue) { // Put the extraData and orderHash on the stack for cheaper access. bytes calldata extraData = zoneParameters.extraData; bytes32 orderHash = zoneParameters.orderHash; @@ -332,6 +332,24 @@ contract ImmutableSignedZoneV3 is } // All 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. + * + * @ 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 pure override returns (bytes4 validOrderMagicValue) { + // All validation done in authoriseOrder. validOrderMagicValue = ZoneInterface.validateOrder.selector; } @@ -411,31 +429,37 @@ contract ImmutableSignedZoneV3 is * @param context Bytes payload of context. * @param zoneParameters The zone parameters. */ - function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) internal pure { + function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) internal /* TODO pure */ { uint256 startIndex = 0; uint256 contextLength = context.length; - // The ImmutableSignedZoneV2 contract enforces at least + // 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); } + emit Dump(uint8(context[0]), startIndex, contextLength); // Each _validateSubstandard* function returns the length of the substandard // segment (0 if the substandard was not matched). startIndex = _validateSubstandard3(context[startIndex:], zoneParameters) + startIndex; + emit Dump(uint8(context[0]), startIndex, contextLength); if (startIndex == contextLength) return; startIndex = _validateSubstandard4(context[startIndex:], zoneParameters) + startIndex; + emit Dump(uint8(context[0]), startIndex, contextLength); if (startIndex == contextLength) return; startIndex = _validateSubstandard6(context[startIndex:], zoneParameters) + startIndex; + emit Dump(uint8(context[0]), startIndex, contextLength); if (startIndex != contextLength) { revert InvalidExtraData("invalid context, unexpected context length", zoneParameters.orderHash); } } + event Dump(uint8, uint256, uint256); + /** * @dev Validates substandard 3. This substandard is used to validate that the server's * specified received items matches the actual received items. This substandard @@ -451,7 +475,7 @@ contract ImmutableSignedZoneV3 is function _validateSubstandard3( bytes calldata context, ZoneParameters calldata zoneParameters - ) internal pure returns (uint256) { + ) internal /* TODO pure */ returns (uint256) { if (uint8(context[0]) != 3) { return 0; } @@ -461,12 +485,16 @@ contract ImmutableSignedZoneV3 is } if (_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1) != bytes32(context[1:33])) { + emit Dump3(_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1), bytes32(context[1:33])); revert Substandard3Violation(zoneParameters.orderHash); } return 33; } +event Dump2(uint256, uint256, uint256); +event Dump3(bytes32, bytes32); + /** * @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 @@ -480,7 +508,7 @@ contract ImmutableSignedZoneV3 is function _validateSubstandard4( bytes calldata context, ZoneParameters calldata zoneParameters - ) internal pure returns (uint256) { + ) internal /*TODO pure */ returns (uint256) { if (uint8(context[0]) != 4) { return 0; } @@ -491,8 +519,11 @@ contract ImmutableSignedZoneV3 is } uint256 expectedOrderHashesSize = uint256(bytes32(context[33:65])); - uint256 substandardIndexEnd = 64 + (expectedOrderHashesSize * 32); - bytes32[] memory expectedOrderHashes = abi.decode(context[1:substandardIndexEnd + 1], (bytes32[])); + uint256 substandardIndexEnd = 65 + (expectedOrderHashesSize * 32); + + emit Dump2(expectedOrderHashesSize, substandardIndexEnd, 0); + bytes32[] memory expectedOrderHashes = abi.decode(context[1:substandardIndexEnd], (bytes32[])); + emit Dump2(expectedOrderHashes.length, zoneParameters.orderHashes.length, 0); // revert if any order hashes in substandard data are not present in zoneParameters.orderHashes. if (!_bytes32ArrayIncludes(zoneParameters.orderHashes, expectedOrderHashes)) { diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol index 34f2b7af..d592d428 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2 // solhint-disable-next-line compiler-version -pragma solidity 0.8.20; +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"; 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..aac28371 100644 --- a/test/trading/seaport/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport/ImmutableSeaportOperational.t.sol @@ -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); } diff --git a/test/trading/seaport16/ImmutableSeaportBase.t.sol b/test/trading/seaport16/ImmutableSeaportBase.t.sol new file mode 100644 index 00000000..827077ca --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportBase.t.sol @@ -0,0 +1,77 @@ +// 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"); + (immutableSigner, immutableSignerPkey) = makeAddrAndKey("immutableSigner"); + (buyer, buyerPkey) = makeAddrAndKey("buyer"); + (seller, sellerPkey) = makeAddrAndKey("seller"); + + // Deploy contracts + immutableSignedZone = new ImmutableSignedZoneV3("ImmutableSignedZone", "", "", owner); + bytes32 zoneManagerRole = immutableSignedZone.ZONE_MANAGER_ROLE(); + vm.prank(owner); + immutableSignedZone.grantRole(zoneManagerRole, zoneManager); + vm.prank(zoneManager); + immutableSignedZone.addSigner(immutableSigner); + + // 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); + + 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..1342c27b --- /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); + } +} \ No newline at end of file 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..b87b7e81 --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportOperational.t.sol @@ -0,0 +1,230 @@ +// 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 = 0xcE810B9B83082C93574784f403727369c3FE6955; + + 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: 0 + }); + + 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..dcd41dad --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol @@ -0,0 +1,901 @@ +// 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); + + // zone + zone = IImmutableSignedZoneV3Harness( + deployCode( + ZONE_ARTIFACT, + abi.encode("MyZoneName", "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); + + // seaport + ConduitController conduitController = new ConduitController(); + seaport = new ImmutableSeaportHarness(address(conduitController), OWNER); + 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..08e710f0 --- /dev/null +++ b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.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 { + bytes internal constant CONSIDERATION_BYTES = + abi.encodePacked("Consideration(", "ReceivedItem[] consideration", ")"); + + bytes internal constant RECEIVED_ITEM_BYTES = + abi.encodePacked( + "ReceivedItem(", + "uint8 itemType,", + "address token,", + "uint256 identifier,", + "uint256 amount,", + "address recipient", + ")" + ); + + bytes32 internal constant RECEIVED_ITEM_TYPEHASH = keccak256(RECEIVED_ITEM_BYTES); + + bytes32 internal constant CONSIDERATION_TYPEHASH = + keccak256(abi.encodePacked(CONSIDERATION_BYTES, RECEIVED_ITEM_BYTES)); + + 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 _convertConsiderationToReceivedItem(ConsiderationItem[] memory _items) internal pure returns (ReceivedItem[] memory) { + ReceivedItem[] memory consideration = new ReceivedItem[](_items.length); + for (uint256 i = 0; i < _items.length; i++) { + consideration[i] = ReceivedItem({ + itemType: _items[i].itemType, + token: _items[i].token, + identifier: _items[i].identifierOrCriteria, + amount: _items[i].startAmount, + recipient: _items[i].recipient + }); + } + return consideration; + } + + 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 _deriveConsiderationHash(ReceivedItem[] calldata consideration) external pure returns (bytes32) { + uint256 numberOfItems = consideration.length; + bytes32[] memory considerationHashes = new bytes32[](numberOfItems); + for (uint256 i; i < numberOfItems; i++) { + considerationHashes[i] = keccak256( + abi.encode( + RECEIVED_ITEM_TYPEHASH, + consideration[i].itemType, + consideration[i].token, + consideration[i].identifier, + consideration[i].amount, + consideration[i].recipient + ) + ); + } + return keccak256(abi.encode(CONSIDERATION_TYPEHASH, keccak256(abi.encodePacked(considerationHashes)))); + } + + 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 view returns (bytes memory) { + return _signOrder(signerPkey, orderHash, 0, ""); + } + + function _signOrder( + 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 + ) + ); + //console.logBytes32(domainSeparator); + + bytes32 structHash = keccak256( + abi.encode( + keccak256("SignedOrder(address fulfiller,uint64 expiration,bytes32 orderHash,bytes context)"), + theFulfiller, + expiration, + orderHash, + keccak256(context) + ) + ); + //console.logBytes32(structHash); + + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", domainSeparator, structHash) + ); + //console.logBytes32(digest); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPkey, digest); + return abi.encodePacked(r, s, v); + } + + function _convertSignatureToEIP2098(bytes calldata signature) external pure returns (bytes memory) { + if (signature.length == 64) { + return signature; + } + if (signature.length != 65) { + revert("Invalid signature length"); + } + return abi.encodePacked(signature[0:64]); + } + + // 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 consideration = _convertConsiderationToReceivedItem(_consideration); + bytes32 considerationHash = this._deriveReceivedItemsHash(consideration); + //bytes32 considerationHash = this._deriveConsiderationHash(consideration); + // TODO Use sub-standard 4 DOES NOT WORK + //bytes32[] memory orderHashes = new bytes32[](1); + //orderHashes[0] = orderHash; + // bytes memory substandard4Data = abi.encode(orderHashes); + // bytes memory context = abi.encodePacked(bytes1(0x04), substandard4Data); + + // Use sub-standard 3 + bytes memory context = abi.encodePacked(bytes1(0x03), considerationHash); + + + bytes memory signature = _signOrder(signerPkey, orderHash, _expiration, context); + return abi.encodePacked( + uint8(0), // SIP6 version + fulfiller, + _expiration, + this._convertSignatureToEIP2098(signature), + context + ); + } + + function _convertToBytesWithoutArrayLength(bytes32[] memory _orders) internal view returns (bytes memory) { + bytes memory data = abi.encodePacked(_orders); + return this._stripArrayLength(data); + } + function _stripArrayLength(bytes calldata _data) external pure returns (bytes memory) { + return _data[32:_data.length]; + } +} \ 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/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..4335ff13 --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol @@ -0,0 +1,64 @@ +// 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) + external + pure; + + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) + external + pure + returns (uint256); + + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters) + external + pure + returns (uint256); + + function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters) + external + /* TODO pure */ + 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..9063c3ae --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -0,0 +1,1519 @@ +// 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"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSignedZoneV3Test is + Test, + SigningTestHelper, + ZoneAccessControlEventsAndErrors, + SIP5EventsAndErrors, + SIP6EventsAndErrors, + SIP7EventsAndErrors +{ + // solhint-disable private-vars-leading-underscore + 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; + // 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", "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", "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, 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", 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); + } + + /* validateOrder */ + + function test_validateOrder_revertsIfEmptyExtraData() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + vm.expectRevert( + abi.encodeWithSelector(InvalidExtraData.selector, "extraData is empty", zoneParameters.orderHash) + ); + zone.authorizeOrder(zoneParameters); + } + + function test_validateOrder_revertsIfExtraDataLengthIsLessThan93() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: bytes(hex"01"), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "extraData length must be at least 93 bytes", zoneParameters.orderHash + ) + ); + zone.authorizeOrder(zoneParameters); + } + + function test_validateOrder_revertsIfExtraDataVersionIsNotSupported() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: bytes( + hex"01f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000660f3027d9ef9e6e50a74cc24433373b9cdd97693a02adcc94e562bb59a5af68190ecaea4414dcbe74618f6c77d11cbcf4a8345bbdf46e665249904925c95929ba6606638b779c6b502204fca6bb0539cdc3dc258fe3ce7b53be0c4ad620899167fedaa8" + ), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + vm.expectRevert(abi.encodeWithSelector(UnsupportedExtraDataVersion.selector, uint8(1))); + zone.authorizeOrder(zoneParameters); + } + + function test_validateOrder_revertsIfSignatureHasExpired() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + uint64 expiration = 100; + + bytes memory extraData = + _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0)); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: extraData, + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + vm.expectRevert( + abi.encodeWithSelector( + SignatureExpired.selector, + 1000, + 100, + bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) + ) + ); + // set current block.timestamp to be 1000 + vm.warp(1000); + zone.authorizeOrder(zoneParameters); + } + + function test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + address randomFulfiller = makeAddr("random"); + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + uint64 expiration = 100; + + bytes memory extraData = + _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0)); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: randomFulfiller, + offerer: OFFERER, + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: extraData, + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + vm.expectRevert( + abi.encodeWithSelector( + InvalidFulfiller.selector, + FULFILLER, + randomFulfiller, + bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) + ) + ); + zone.validateOrder(zoneParameters); + } + + function test_validateOrder_revertsIfSignerIsNotActive() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + // no signer added + + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + uint64 expiration = 100; + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked( + bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + ); + + bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: spentItems, + consideration: receivedItems, + extraData: extraData, + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + vm.expectRevert( + abi.encodeWithSelector(SignerNotActive.selector, address(0x6E12D8C87503D4287c294f2Fdef96ACd9DFf6bd2)) + ); + zone.authorizeOrder(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); + + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + uint64 expiration = 100; + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + bytes memory extraData = + _buildExtraDataWithoutContext(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0)); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: spentItems, + consideration: receivedItems, + extraData: extraData, + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash + ) + ); + zone.authorizeOrder(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); + + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + uint64 expiration = 100; + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked( + bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + ); + + bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: spentItems, + consideration: receivedItems, + extraData: extraData, + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + assertEq(zone.authorizeOrder(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(0x3839be19)); // SIP-5 compliance - ZoneInterface + } + + /* _domainSeparator */ + + function test_domainSeparator_returnsCachedDomainSeparatorWhenChainIDMatchesValueSetOnDeployment() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32 domainSeparator = zone.exposed_domainSeparator(); + assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + } + + function test_domainSeparator_returnsUpdatedDomainSeparatorIfChainIDIsDifferentFromValueSetOnDeployment() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32 domainSeparatorCached = zone.exposed_domainSeparator(); + vm.chainId(31338); + bytes32 domainSeparatorDerived = zone.exposed_domainSeparator(); + + assertNotEq(domainSeparatorCached, domainSeparatorDerived); + assertEq(domainSeparatorDerived, bytes32(0x835aabb0d2af048df195a75a990b42533471d4a4e82842cd54a892eaac463d74)); + } + + /* _deriveDomainSeparator */ + + function test_deriveDomainSeparator_returnsDomainSeparatorForChainID() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32 domainSeparator = zone.exposed_deriveDomainSeparator(); + assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + } + + /* _getSupportedSubstandards */ + + function test_getSupportedSubstandards() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + uint256[] memory supportedSubstandards = zone.exposed_getSupportedSubstandards(); + assertEq(supportedSubstandards.length, 3); + assertEq(supportedSubstandards[0], 3); + assertEq(supportedSubstandards[1], 4); + assertEq(supportedSubstandards[2], 6); + } + + /* _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 = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash + ) + ); + + zone.exposed_validateSubstandards(new bytes(0), zoneParameters); + } + + function test_validateSubstandards_substandard3() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data); + zone.exposed_validateSubstandards(context, zoneParameters); + } + + function test_validateSubstandards_substandard4() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked( + bytes1(0x04), + bytes32(uint256(32)), + bytes32(uint256(1)), + bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) + ); + + zone.exposed_validateSubstandards(context, zoneParameters); + } + + function test_validateSubstandards_substandard6() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x2), identifier: 222, amount: 10}); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 100, 10)); + bytes32 substandard6Data = 0x6d0303fb2c992bf1970cab0fae2e4cd817df77741cee30dd7917b719a165af3e; + bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), substandard6Data); + + zone.exposed_validateSubstandards(context, zoneParameters); + } + + function test_validateSubstandards_multipleSubstandardsInCorrectOrder() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data); + + zone.exposed_validateSubstandards(context, zoneParameters); + } + + function test_validateSubstandards_substandards3Then6() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x06), substandard6Data); + + zone.exposed_validateSubstandards(context, zoneParameters); + } + + function test_validateSubstandards_allSubstandards() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked( + bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + ); + + zone.exposed_validateSubstandards(context, zoneParameters); + } + + function test_validateSubstandards_revertsOnMultipleSubstandardsInIncorrectOrder() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory context = abi.encodePacked(bytes1(0x04), substandard4Data, bytes1(0x03), substandard3Data); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid context, unexpected context length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandards(context, zoneParameters); + } + + /* _validateSubstandard3 */ + + function test_validateSubstandard3_returnsZeroLengthIfNotSubstandard3() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(hex"04", zoneParameters); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard3_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x03), bytes10(0)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 3 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard3(context, zoneParameters); + } + + function test_validateSubstandard3_revertsIfDerivedReceivedItemsHashNotEqualToHashInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x03), bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(Substandard3Violation.selector, zoneParameters.orderHash)); + zone.exposed_validateSubstandard3(context, zoneParameters); + } + + function test_validateSubstandard3_returns33OnSuccess() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); + bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(context, zoneParameters); + assertEq(substandardLengthResult, 33); + } + + /* _validateSubstandard4 */ + + function test_validateSubstandard4_returnsZeroLengthIfNotSubstandard4() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(hex"02", zoneParameters); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard4_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x04), bytes10(0)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 4 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard4(context, zoneParameters); + } + + function test_validateSubstandard4_revertsIfExpectedOrderHashesAreNotPresent() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes32[] memory expectedOrderHashes = new bytes32[](1); + expectedOrderHashes[0] = bytes32(0x17d4cf2b6c174a86b533210b50ba676a82e5ab1e2e89ea538f0a43a37f92fcbf); + + bytes memory context = abi.encodePacked(bytes1(0x04), abi.encode(expectedOrderHashes)); + + vm.expectRevert( + abi.encodeWithSelector( + Substandard4Violation.selector, + zoneParameters.orderHashes, + expectedOrderHashes, + zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard4(context, zoneParameters); + } + + function test_validateSubstandard4_returnsLengthOfSubstandardSegmentOnSuccess() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x04), abi.encode(orderHashes)); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(context, zoneParameters); + // bytes1 + bytes32 + bytes32 + bytes32 = 97 + assertEq(substandardLengthResult, 97); + } + + /* _validateSubstandard6 */ + + function test_validateSubstandard6_returnsZeroLengthIfNotSubstandard6() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(hex"04", zoneParameters); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard6_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x06), bytes10(0)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 6 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard6(context, zoneParameters); + } + + function test_validateSubstandard6_revertsIfDerivedReceivedItemsHashesIsNotEqualToHashesInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x2), identifier: 222, amount: 10}); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), bytes32(uint256(0x123456))); + + vm.expectRevert( + abi.encodeWithSelector(Substandard6Violation.selector, spentItems[0].amount, 100, zoneParameters.orderHash) + ); + zone.exposed_validateSubstandard6(context, zoneParameters); + } + + function test_validateSubstandard6_returnsLengthOfSubstandardSegmentOnSuccess() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x2), identifier: 222, amount: 10}); + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + receivedItems[0] = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 222, + amount: 10, + recipient: payable(address(0x3)) + }); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 100, 10)); + bytes32 substandard6Data = 0x6d0303fb2c992bf1970cab0fae2e4cd817df77741cee30dd7917b719a165af3e; + bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), substandard6Data); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(context, zoneParameters); + // bytes1 + uint256 + bytes32 = 65 + assertEq(substandardLengthResult, 65); + } + + /* _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", "https://www.immutable.com", "https://www.immutable.com/docs", owner + ); + } + + function _newZoneHarness(address owner) private returns (ImmutableSignedZoneV3Harness) { + return new ImmutableSignedZoneV3Harness( + "MyZoneName", "https://www.immutable.com", "https://www.immutable.com/docs", owner + ); + } + + 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; + } + + function _buildExtraDataWithoutContext( + 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)) + ); + 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..09b29be1 --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -0,0 +1,87 @@ +// 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, string memory apiEndpoint, string memory documentationURI, address owner) + ImmutableSignedZoneV3(zoneName, 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) + external + /* TODO pure */ + { + return _validateSubstandards(context, zoneParameters); + } + + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) + external + /* TODO pure */ + returns (uint256) + { + return _validateSubstandard3(context, zoneParameters); + } + + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters) + external + /* TODO pure */ + returns (uint256) + { + return _validateSubstandard4(context, zoneParameters); + } + + function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters) + external + pure + returns (uint256) + { + return _validateSubstandard6(context, zoneParameters); + } + + 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..7c59b662 --- /dev/null +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -0,0 +1,102 @@ +# Test Plan for Immutable Signed Zone (v2) + +## ImmutableSignedZoneV2.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_validateOrder_revertsIfEmptyExtraData` | Validate order with empty `extraData`. | No | Yes | +| `test_validateOrder_revertsIfExtraDataLengthIsLessThan93` | Validate order with unexpected `extraData` length. | No | Yes | +| `test_validateOrder_revertsIfExtraDataVersionIsNotSupported` | Validate order with unexpected SIP-6 version byte. | No | Yes | +| `test_validateOrder_revertsIfSignatureHasExpired` | Validate order with an expired signature. | No | Yes | +| `test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Validate order with unexpected fufiller. | No | Yes | +| `test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Validate order with expected *any* fufiller. | Yes | No | +| `test_validateOrder_revertsIfSignerIsNotActive` | Validate order with inactive signer. | No | 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 should revert. | No | Yes | +| `test_validateSubstandards_substandard3` | Context with substandard 3. | Yes | Yes | +| `test_validateSubstandards_substandard4` | Context with substandard 4. | Yes | Yes | +| `test_validateSubstandards_substandard6` | Context with substandard 6. | Yes | Yes | +| `test_validateSubstandards_multipleSubstandardsInCorrectOrder` | Context with multiple substandards. | Yes | Yes | +| `test_validateSubstandards_substandards3Then6` | Context with substandards 3 and 6, but not 4. | Yes | Yes | +| `test_validateSubstandards_allSubstandards` | Context with all substandards. | Yes | Yes | +| `test_validateSubstandards_revertsOnMultipleSubstandardsInIncorrectOrder` | Context with multiple substandards out of order. | No | 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_returns33OnSuccess` | Substandard 3 validation when derived hash matches expected hash. | 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_returnsLengthOfSubstandardSegmentOnSuccess` | 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_returnsLengthOfSubstandardSegmentOnSuccess` | Substandard 6 validation when derived hash matches expected hash. | 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/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol](../../../ImmutableSeaportSignedZoneV2Integration.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 | From 73a1437620378b46ed49010dfa5c1ac97381aadd Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 30 Oct 2025 13:59:28 +1100 Subject: [PATCH 04/45] Use deterministic deployer address when testing domain separator logic --- .../v2/ImmutableSignedZoneV2.t.sol | 15 ++++++++++++--- .../v3/ImmutableSignedZoneV3.t.sol | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) 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..e200d08a 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 */ 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 index 9063c3ae..027e7b04 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -683,30 +683,39 @@ contract ImmutableSignedZoneV3Test is /* _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(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + 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(0x835aabb0d2af048df195a75a990b42533471d4a4e82842cd54a892eaac463d74)); + 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(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + assertEq(domainSeparator, bytes32(0x7946de4e45fec74bd718eeaf23c0d6e9ee4a66d31dcaa416695b27a794624c58)); } /* _getSupportedSubstandards */ From b62f3e8c1b8974d4ff720f406103bb4f30495f8d Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 30 Oct 2025 14:00:06 +1100 Subject: [PATCH 05/45] Update ZoneInterface ID for Seaport 1.6 zones --- .../zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 027e7b04..0c273a8d 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -677,7 +677,7 @@ contract ImmutableSignedZoneV3Test is assertTrue(zone.supportsInterface(0x01ffc9a7)); // ERC165 interface assertFalse(zone.supportsInterface(0xffffffff)); // ERC165 compliance assertTrue(zone.supportsInterface(0x2e778efc)); // SIP-5 interface - assertTrue(zone.supportsInterface(0x3839be19)); // SIP-5 compliance - ZoneInterface + assertTrue(zone.supportsInterface(0x39dd6933)); // SIP-5 compliance - ZoneInterface } /* _domainSeparator */ From 9f821574412e98a1c822367eb80689ea1a55f346 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 30 Oct 2025 14:11:12 +1100 Subject: [PATCH 06/45] Cleanup TODOs and metadata --- .../v3/ImmutableSignedZoneV3.sol | 27 +++++-------------- .../v3/ImmutableSignedZoneV3Harness.t.sol | 6 ++--- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index b20462be..6ffb983f 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -19,7 +19,7 @@ import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; /** * @title ImmutableSignedZoneV3 * @author Immutable - * @notice ImmutableSignedZone32 is a zone implementation based on the + * @notice ImmutableSignedZone3 is a zone implementation based on the * SIP-7 standard https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-7.md * implementing substandards 3, 4 and 6. * @@ -256,7 +256,7 @@ contract ImmutableSignedZoneV3 is */ function authorizeOrder( ZoneParameters calldata zoneParameters - ) external override /* TODO view */ returns (bytes4 authorizedOrderMagicValue) { + ) external override returns (bytes4 authorizedOrderMagicValue) { // Put the extraData and orderHash on the stack for cheaper access. bytes calldata extraData = zoneParameters.extraData; bytes32 orderHash = zoneParameters.orderHash; @@ -341,8 +341,8 @@ contract ImmutableSignedZoneV3 is * @dev This function is called by Seaport whenever any extraData is * provided by the caller. * - * @ param zoneParameters The zone parameters containing data related to - * the fulfilment execution. + * @param zoneParameters The zone parameters containing data related to + * the fulfilment execution. * @return validOrderMagicValue A magic value indicating if the order is * currently valid. */ @@ -429,7 +429,7 @@ contract ImmutableSignedZoneV3 is * @param context Bytes payload of context. * @param zoneParameters The zone parameters. */ - function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) internal /* TODO pure */ { + function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) internal pure { uint256 startIndex = 0; uint256 contextLength = context.length; @@ -438,28 +438,22 @@ contract ImmutableSignedZoneV3 is if (contextLength == 0) { revert InvalidExtraData("invalid context, no substandards present", zoneParameters.orderHash); } - emit Dump(uint8(context[0]), startIndex, contextLength); // Each _validateSubstandard* function returns the length of the substandard // segment (0 if the substandard was not matched). startIndex = _validateSubstandard3(context[startIndex:], zoneParameters) + startIndex; - emit Dump(uint8(context[0]), startIndex, contextLength); if (startIndex == contextLength) return; startIndex = _validateSubstandard4(context[startIndex:], zoneParameters) + startIndex; - emit Dump(uint8(context[0]), startIndex, contextLength); if (startIndex == contextLength) return; startIndex = _validateSubstandard6(context[startIndex:], zoneParameters) + startIndex; - emit Dump(uint8(context[0]), startIndex, contextLength); if (startIndex != contextLength) { revert InvalidExtraData("invalid context, unexpected context length", zoneParameters.orderHash); } } - event Dump(uint8, uint256, uint256); - /** * @dev Validates substandard 3. This substandard is used to validate that the server's * specified received items matches the actual received items. This substandard @@ -475,7 +469,7 @@ contract ImmutableSignedZoneV3 is function _validateSubstandard3( bytes calldata context, ZoneParameters calldata zoneParameters - ) internal /* TODO pure */ returns (uint256) { + ) internal pure returns (uint256) { if (uint8(context[0]) != 3) { return 0; } @@ -485,16 +479,12 @@ contract ImmutableSignedZoneV3 is } if (_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1) != bytes32(context[1:33])) { - emit Dump3(_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1), bytes32(context[1:33])); revert Substandard3Violation(zoneParameters.orderHash); } return 33; } -event Dump2(uint256, uint256, uint256); -event Dump3(bytes32, bytes32); - /** * @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 @@ -508,7 +498,7 @@ event Dump3(bytes32, bytes32); function _validateSubstandard4( bytes calldata context, ZoneParameters calldata zoneParameters - ) internal /*TODO pure */ returns (uint256) { + ) internal pure returns (uint256) { if (uint8(context[0]) != 4) { return 0; } @@ -520,10 +510,7 @@ event Dump3(bytes32, bytes32); uint256 expectedOrderHashesSize = uint256(bytes32(context[33:65])); uint256 substandardIndexEnd = 65 + (expectedOrderHashesSize * 32); - - emit Dump2(expectedOrderHashesSize, substandardIndexEnd, 0); bytes32[] memory expectedOrderHashes = abi.decode(context[1:substandardIndexEnd], (bytes32[])); - emit Dump2(expectedOrderHashes.length, zoneParameters.orderHashes.length, 0); // revert if any order hashes in substandard data are not present in zoneParameters.orderHashes. if (!_bytes32ArrayIncludes(zoneParameters.orderHashes, expectedOrderHashes)) { 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 index 09b29be1..59bccb49 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -38,14 +38,14 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) external - /* TODO pure */ + pure { return _validateSubstandards(context, zoneParameters); } function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) external - /* TODO pure */ + pure returns (uint256) { return _validateSubstandard3(context, zoneParameters); @@ -53,7 +53,7 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters) external - /* TODO pure */ + pure returns (uint256) { return _validateSubstandard4(context, zoneParameters); From 5021a0dd37746b818dc551346ce4e68086944d9a Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 30 Oct 2025 15:18:28 +1100 Subject: [PATCH 07/45] Fix for SIP-7 substandard 4 --- .../v3/ImmutableSignedZoneV3.sol | 35 ++++++--- .../v2/ImmutableSignedZoneV2.t.sol | 1 + .../v3/ImmutableSignedZoneV3.t.sol | 73 ++++++++++++++++--- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 6ffb983f..52a6e15a 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -247,10 +247,12 @@ contract ImmutableSignedZoneV3 is * @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. + * provided by the caller, before tokens have been transferred. Note + * that this function does not validate the SIP-7 substandards. Validation + * of the SIP-7 substandards is performed in the validateOrder function. * - * @param zoneParameters The zone parameters containing data related to - * the fulfilment execution. + * @param zoneParameters The zone parameters containing data related to + * the fulfilment execution. * @return authorizedOrderMagicValue A magic value indicating if the order * is currently valid. */ @@ -311,9 +313,6 @@ contract ImmutableSignedZoneV3 is revert InvalidFulfiller(expectedFulfiller, actualFulfiller, orderHash); } - // Validate supported substandards. - _validateSubstandards(context, zoneParameters); - // Derive the signedOrder hash. bytes32 signedOrderHash = _deriveSignedOrderHash(expectedFulfiller, expiration, orderHash, context); @@ -331,7 +330,7 @@ contract ImmutableSignedZoneV3 is revert SignerNotActive(recoveredSigner); } - // All validation completes and passes with no reverts, return valid. + // Pre hook validation completes and passes with no reverts, return valid. authorizedOrderMagicValue = ZoneInterface.authorizeOrder.selector; } @@ -339,7 +338,10 @@ contract ImmutableSignedZoneV3 is * @notice Validates a fulfilment execution. * * @dev This function is called by Seaport whenever any extraData is - * provided by the caller. + * provided by the caller, after tokens have been transferred. + * Note that this function only validates the SIP-7 substandards as + * the final value of ZoneParameters.orderHashes is not known in the + * authorizeOrder function. * * @param zoneParameters The zone parameters containing data related to * the fulfilment execution. @@ -347,9 +349,18 @@ contract ImmutableSignedZoneV3 is * currently valid. */ function validateOrder( - ZoneParameters calldata /* zoneParameters */ + ZoneParameters calldata zoneParameters ) external pure override returns (bytes4 validOrderMagicValue) { - // All validation done in authoriseOrder. + // Put the extraData and orderHash on the stack for cheaper access. + bytes calldata extraData = zoneParameters.extraData; + + // extraData bytes 93-end: context (optional, variable length). + bytes calldata context = extraData[93:]; + + // Validate supported substandards. + _validateSubstandards(context, zoneParameters); + + // Pre hook validation completes and passes with no reverts, return valid. validOrderMagicValue = ZoneInterface.validateOrder.selector; } @@ -509,8 +520,8 @@ contract ImmutableSignedZoneV3 is } uint256 expectedOrderHashesSize = uint256(bytes32(context[33:65])); - uint256 substandardIndexEnd = 65 + (expectedOrderHashesSize * 32); - bytes32[] memory expectedOrderHashes = abi.decode(context[1:substandardIndexEnd], (bytes32[])); + uint256 substandardIndexEnd = 64 + (expectedOrderHashesSize * 32); + 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)) { 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 e200d08a..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 @@ -742,6 +742,7 @@ contract ImmutableSignedZoneV2Test is } /* _validateSubstandards */ + function test_validateSubstandards_revertsIfEmptyContext() public { ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); 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 index 0c273a8d..3a23b94b 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -389,7 +389,7 @@ contract ImmutableSignedZoneV3Test is /* validateOrder */ - function test_validateOrder_revertsIfEmptyExtraData() public { + function test_authorizeOrder_revertsIfEmptyExtraData() public { ImmutableSignedZoneV3 zone = _newZone(OWNER); ZoneParameters memory zoneParameters = ZoneParameters({ orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), @@ -409,7 +409,7 @@ contract ImmutableSignedZoneV3Test is zone.authorizeOrder(zoneParameters); } - function test_validateOrder_revertsIfExtraDataLengthIsLessThan93() public { + function test_authorizeOrder_revertsIfExtraDataLengthIsLessThan93() public { ImmutableSignedZoneV3 zone = _newZone(OWNER); ZoneParameters memory zoneParameters = ZoneParameters({ orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), @@ -431,7 +431,7 @@ contract ImmutableSignedZoneV3Test is zone.authorizeOrder(zoneParameters); } - function test_validateOrder_revertsIfExtraDataVersionIsNotSupported() public { + function test_authorizeOrder_revertsIfExtraDataVersionIsNotSupported() public { ImmutableSignedZoneV3 zone = _newZone(OWNER); ZoneParameters memory zoneParameters = ZoneParameters({ orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), @@ -451,7 +451,7 @@ contract ImmutableSignedZoneV3Test is zone.authorizeOrder(zoneParameters); } - function test_validateOrder_revertsIfSignatureHasExpired() public { + function test_authorizeOrder_revertsIfSignatureHasExpired() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); uint64 expiration = 100; @@ -484,7 +484,7 @@ contract ImmutableSignedZoneV3Test is zone.authorizeOrder(zoneParameters); } - function test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller() public { + function test_authorizeOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); address randomFulfiller = makeAddr("random"); bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); @@ -513,10 +513,10 @@ contract ImmutableSignedZoneV3Test is bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) ) ); - zone.validateOrder(zoneParameters); + zone.authorizeOrder(zoneParameters); } - function test_validateOrder_revertsIfSignerIsNotActive() public { + function test_authorizeOrder_revertsIfSignerIsNotActive() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); // no signer added @@ -567,6 +567,60 @@ contract ImmutableSignedZoneV3Test is 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); + + bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + uint64 expiration = 100; + + SpentItem[] memory spentItems = new SpentItem[](1); + spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + + 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; + + bytes32[] memory orderHashes = new bytes32[](1); + orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + + // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); + bytes memory substandard4Data = abi.encode(orderHashes); + bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); + bytes memory context = abi.encodePacked( + bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + ); + + bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), + fulfiller: FULFILLER, + offerer: OFFERER, + offer: spentItems, + consideration: receivedItems, + extraData: extraData, + orderHashes: orderHashes, + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + assertEq(zone.authorizeOrder(zoneParameters), bytes4(0x01e4d72a)); + } + + /* validateOrder */ + function test_validateOrder_revertsIfContextIsEmpty() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); @@ -615,7 +669,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash ) ); - zone.authorizeOrder(zoneParameters); + zone.validateOrder(zoneParameters); } function test_validateOrder_returnsMagicValueOnSuccessfulValidation() public { @@ -667,7 +721,7 @@ contract ImmutableSignedZoneV3Test is endTime: 0, zoneHash: bytes32(0) }); - assertEq(zone.authorizeOrder(zoneParameters), bytes4(0x17b1f942)); + assertEq(zone.validateOrder(zoneParameters), bytes4(0x17b1f942)); } /* supportsInterface */ @@ -742,6 +796,7 @@ contract ImmutableSignedZoneV3Test is } /* _validateSubstandards */ + function test_validateSubstandards_revertsIfEmptyContext() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); From 7762672e57a87910334e5468d95d3cae33fd4911 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 30 Oct 2025 15:27:29 +1100 Subject: [PATCH 08/45] Readme update --- .../v3/ImmutableSignedZoneV3.sol | 2 +- .../zones/immutable-signed-zone/v3/README.md | 34 ++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 52a6e15a..90efb2c8 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -19,7 +19,7 @@ import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; /** * @title ImmutableSignedZoneV3 * @author Immutable - * @notice ImmutableSignedZone3 is a zone implementation based on the + * @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 3, 4 and 6. * diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md index 59f9301a..f5cb3ab6 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -1,4 +1,4 @@ -# Immutable Signed Zone (v2) +# 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) 3, 4 and 6. @@ -31,13 +31,14 @@ The trading system on the Immutable platform is shown in the diagram below. flowchart LR client[Client] <-- 1. POST .../fulfillment-data ---> ob[Immutable Off-Chain\nOrderbook] client -- 2. fulfillAdvancedOrder ---> seaport[ImmutableSeaport.sol] - seaport -- 3a. transferFrom --> erc20[IERC20.sol] - seaport -- 3b. transferFrom --> erc721[IERC721.sol] - seaport -- 3c. safeTransferFrom --> erc1155[IERC1155.sol] - seaport -- 4. validateOrder --> Zone + 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[ImmutableSignedZoneV2.sol] --> AccessControlEnumerable.sol + zone[ImmutableSignedZoneV3.sol] --> AccessControlEnumerable.sol end ``` @@ -46,19 +47,12 @@ 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 `validateOrder` on `ImmutableSignedZoneV2.sol`, passing it the fulfilment execution details as well as the `extraData` parameter -5. `ImmutableSignedZoneV2.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met +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 (v1) +## Differences compared to ImmutableSignedZone (v2) -The contract was developed based on ImmutableSignedZone, with the addition of: - - SIP7 substandard 6 support - - Role based access control to be role based - -### ZoneAccessControl - -The contract now uses a finer grained access control with role based access with the `ZoneAccessControl` interface, rather than the `Ownable` interface in the v1 contract. A separate `zoneManager` roles is used to manage signers and an admin role used to control roles. - -### Support of SIP7 substandard 6 - -The V2 contract now supports substandard-6 of the SIP7 specification, found here (https://github.com/immutable/platform-services/pull/12775). A server side signed order can adhere to substandard 3 + 4 (full fulfillment only) or substandard 6 + 4 (full or partial fulfillment). +The contract was developed based on ImmutableSignedZoneV2, with the addition of: + - Support for the Seaport 1.6 Zone interface `authorizeOrder` function From 9e9e3c972f8209fdeae88cc7e14760ccb2291543 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 30 Oct 2025 16:31:36 +1100 Subject: [PATCH 09/45] Update test plan readme --- .../seaport16/ImmutableSeaportBase.t.sol | 3 +- .../seaport16/ImmutableSeaportConfig.t.sol | 2 +- .../v3/IImmutableSignedZoneV3Harness.t.sol | 2 +- .../zones/immutable-signed-zone/v3/README.md | 34 +++++++++---------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/test/trading/seaport16/ImmutableSeaportBase.t.sol b/test/trading/seaport16/ImmutableSeaportBase.t.sol index 827077ca..dbeb1240 100644 --- a/test/trading/seaport16/ImmutableSeaportBase.t.sol +++ b/test/trading/seaport16/ImmutableSeaportBase.t.sol @@ -42,9 +42,10 @@ abstract contract ImmutableSeaportBaseTest is Test { 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"); diff --git a/test/trading/seaport16/ImmutableSeaportConfig.t.sol b/test/trading/seaport16/ImmutableSeaportConfig.t.sol index 1342c27b..1d85ac14 100644 --- a/test/trading/seaport16/ImmutableSeaportConfig.t.sol +++ b/test/trading/seaport16/ImmutableSeaportConfig.t.sol @@ -14,4 +14,4 @@ contract ImmutableSeaportConfigTest is ImmutableSeaportBaseTest { emit AllowedZoneSet(zone, allowed); immutableSeaport.setAllowedZone(zone, allowed); } -} \ No newline at end of file +} 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 index 4335ff13..f73c5116 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol @@ -46,7 +46,7 @@ interface IImmutableSignedZoneV3Harness is ZoneInterface, SIP7Interface { function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters) external - /* TODO pure */ + pure returns (uint256); function exposed_deriveReceivedItemsHash( diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index 7c59b662..a51a4f63 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -1,6 +1,6 @@ -# Test Plan for Immutable Signed Zone (v2) +# Test Plan for Immutable Signed Zone (v3) -## ImmutableSignedZoneV2.sol +## ImmutableSignedZoneV3.sol Constructor tests: @@ -38,20 +38,20 @@ Control function tests: 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_validateOrder_revertsIfEmptyExtraData` | Validate order with empty `extraData`. | No | Yes | -| `test_validateOrder_revertsIfExtraDataLengthIsLessThan93` | Validate order with unexpected `extraData` length. | No | Yes | -| `test_validateOrder_revertsIfExtraDataVersionIsNotSupported` | Validate order with unexpected SIP-6 version byte. | No | Yes | -| `test_validateOrder_revertsIfSignatureHasExpired` | Validate order with an expired signature. | No | Yes | -| `test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Validate order with unexpected fufiller. | No | Yes | -| `test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller` | Validate order with expected *any* fufiller. | Yes | No | -| `test_validateOrder_revertsIfSignerIsNotActive` | Validate order with inactive signer. | No | Yes | -| `test_validateOrder_revertsIfContextIsEmpty` | Validate order with an empty context. | No | Yes | -| `test_validateOrder_returnsMagicValueOnSuccessfulValidation` | Validate order successfully. | Yes | Yes | +| 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_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_revertsIfSignerIsNotActive` | Authorize order with inactive signer. | No | Yes | +| `test_authorizeOrder_returnsMagicValueOnSuccessfulValidation` | Authorize order successfully. | 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: @@ -92,7 +92,7 @@ Internal operational function tests: Integration tests: -All of these tests are in [test/trading/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol](../../../ImmutableSeaportSignedZoneV2Integration.t.sol). +All of these tests are in [test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol](../../../ImmutableSeaportSignedZoneV3Integration.t.sol). | Test name | Description | Happy Case | Implemented | | ---------------------------------------------------- | ------------------------------- | ---------- | ----------- | From a1ad21c6f44ddd013c53a87ad31ecca8fa8c81ed Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 31 Oct 2025 12:36:53 +1100 Subject: [PATCH 10/45] Update Seaport operational tests --- .../seaport/ImmutableSeaportOperational.t.sol | 316 +++++++++--------- .../ImmutableSeaportOperational.t.sol | 11 +- .../ImmutableSeaportTestHelper.t.sol | 113 ++----- .../seaport16/utils/SigningTestHelper.t.sol | 24 ++ 4 files changed, 210 insertions(+), 254 deletions(-) create mode 100644 test/trading/seaport16/utils/SigningTestHelper.t.sol diff --git a/test/trading/seaport/ImmutableSeaportOperational.t.sol b/test/trading/seaport/ImmutableSeaportOperational.t.sol index aac28371..f06c48c2 100644 --- a/test/trading/seaport/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport/ImmutableSeaportOperational.t.sol @@ -63,168 +63,168 @@ contract SellerWallet { -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); +// contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableSeaportTestHelper { +// SellerWallet public sellerWallet; +// TestERC721 public erc721; +// uint256 public nftId; - vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.InvalidZone.selector, randomZone)); - immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); - } +// function setUp() public override { +// super.setUp(); +// _setFulfillerAndZone(buyer, address(immutableSignedZone)); +// sellerWallet = new SellerWallet(); +// nftId = 1; +// vm.deal(buyer, 10 ether); +// } - 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(); +// function testFulfillFullRestrictedOrder() public { +// _checkFulfill(OrderType.FULL_RESTRICTED); +// } - vm.prank(owner); - immutableSeaport.setAllowedZone(address(immutableSignedZone), false); +// function testFulfillPartialRestrictedOrder() public { +// _checkFulfill(OrderType.PARTIAL_RESTRICTED); +// } - 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 = 0xcE810B9B83082C93574784f403727369c3FE6955; - - vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); - immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); - } +// function testRejectUnsupportedZones() public { +// // Create order with random zone +// address randomZone = makeAddr("randomZone"); +// AdvancedOrder memory order = _prepareCheckFulfill(randomZone); - 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: 0 - }); - - 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 +// 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 = 0xcE810B9B83082C93574784f403727369c3FE6955; + +// 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: 0 +// }); + +// 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/ImmutableSeaportOperational.t.sol b/test/trading/seaport16/ImmutableSeaportOperational.t.sol index b87b7e81..47e57d57 100644 --- a/test/trading/seaport16/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport16/ImmutableSeaportOperational.t.sol @@ -85,7 +85,6 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS _checkFulfill(OrderType.PARTIAL_RESTRICTED); } - function testRejectUnsupportedZones() public { // Create order with random zone address randomZone = makeAddr("randomZone"); @@ -119,7 +118,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,9 +130,9 @@ 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; + address derivedBadSigner = 0x7D86d2b5A73f1620093012C73B3a99781B11B2F5; vm.prank(buyer); vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); @@ -213,7 +212,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS zoneHash: orderParams.zoneHash, salt: orderParams.salt, conduitKey: orderParams.conduitKey, - counter: 0 + counter: immutableSeaport.getCounter(orderParams.offerer) }); bytes32 orderHash = immutableSeaport.getOrderHash(orderComponents); @@ -227,4 +226,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/seaport16/ImmutableSeaportTestHelper.t.sol b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol index 08e710f0..cece803f 100644 --- a/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol +++ b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol @@ -2,31 +2,15 @@ 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 { - bytes internal constant CONSIDERATION_BYTES = - abi.encodePacked("Consideration(", "ReceivedItem[] consideration", ")"); - - bytes internal constant RECEIVED_ITEM_BYTES = - abi.encodePacked( - "ReceivedItem(", - "uint8 itemType,", - "address token,", - "uint256 identifier,", - "uint256 amount,", - "address recipient", - ")" - ); - - bytes32 internal constant RECEIVED_ITEM_TYPEHASH = keccak256(RECEIVED_ITEM_BYTES); - - bytes32 internal constant CONSIDERATION_TYPEHASH = - keccak256(abi.encodePacked(CONSIDERATION_BYTES, RECEIVED_ITEM_BYTES)); +abstract contract ImmutableSeaportTestHelper is Test, SigningTestHelper { string public constant ZONE_NAME = "ImmutableSignedZone"; string public constant VERSION = "3.0"; @@ -39,7 +23,6 @@ abstract contract ImmutableSeaportTestHelper is Test { theZone = _zone; } - // Helper functions function _createZoneParameters(bytes memory _extraData) internal returns (ZoneParameters memory) { bytes32 orderHash = keccak256("0x1234"); @@ -87,10 +70,10 @@ abstract contract ImmutableSeaportTestHelper is Test { return consideration; } - function _convertConsiderationToReceivedItem(ConsiderationItem[] memory _items) internal pure returns (ReceivedItem[] memory) { - ReceivedItem[] memory consideration = new ReceivedItem[](_items.length); + 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++) { - consideration[i] = ReceivedItem({ + receivedItems[i] = ReceivedItem({ itemType: _items[i].itemType, token: _items[i].token, identifier: _items[i].identifierOrCriteria, @@ -98,7 +81,7 @@ abstract contract ImmutableSeaportTestHelper is Test { recipient: _items[i].recipient }); } - return consideration; + return receivedItems; } function _createConsiderationItems(address recipient, uint256 amount) internal pure returns (ConsiderationItem[] memory) { @@ -114,31 +97,12 @@ abstract contract ImmutableSeaportTestHelper is Test { return consideration; } - function _deriveConsiderationHash(ReceivedItem[] calldata consideration) external pure returns (bytes32) { - uint256 numberOfItems = consideration.length; - bytes32[] memory considerationHashes = new bytes32[](numberOfItems); - for (uint256 i; i < numberOfItems; i++) { - considerationHashes[i] = keccak256( - abi.encode( - RECEIVED_ITEM_TYPEHASH, - consideration[i].itemType, - consideration[i].token, - consideration[i].identifier, - consideration[i].amount, - consideration[i].recipient - ) - ); - } - return keccak256(abi.encode(CONSIDERATION_TYPEHASH, keccak256(abi.encodePacked(considerationHashes)))); - } - function _deriveReceivedItemsHash( ReceivedItem[] calldata receivedItems ) public pure returns (bytes32) { return _deriveReceivedItemsHash(receivedItems, 1, 1); } - function _deriveReceivedItemsHash( ReceivedItem[] calldata receivedItems, uint256 scalingFactorNumerator, @@ -161,12 +125,12 @@ abstract contract ImmutableSeaportTestHelper is Test { return keccak256(receivedItemsHash); } - - function _signOrder(uint256 signerPkey, bytes32 orderHash) internal view returns (bytes memory) { - return _signOrder(signerPkey, orderHash, 0, ""); + function _signOrder(uint256 _signerPkey, bytes32 _orderHash) internal view returns (bytes memory) { + // For the purposes of testing, the offerer wallet will always return valid for signature checks + return abi.encodePacked("Hello!"); } - function _signOrder( + function _signSIP7Order( uint256 _signerPkey, bytes32 orderHash, uint64 expiration, @@ -182,7 +146,6 @@ abstract contract ImmutableSeaportTestHelper is Test { theZone ) ); - //console.logBytes32(domainSeparator); bytes32 structHash = keccak256( abi.encode( @@ -193,25 +156,10 @@ abstract contract ImmutableSeaportTestHelper is Test { keccak256(context) ) ); - //console.logBytes32(structHash); - bytes32 digest = keccak256( - abi.encodePacked("\x19\x01", domainSeparator, structHash) - ); - //console.logBytes32(digest); + bytes32 digest = ECDSA.toTypedDataHash(domainSeparator, structHash); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPkey, digest); - return abi.encodePacked(r, s, v); - } - - function _convertSignatureToEIP2098(bytes calldata signature) external pure returns (bytes memory) { - if (signature.length == 64) { - return signature; - } - if (signature.length != 65) { - revert("Invalid signature length"); - } - return abi.encodePacked(signature[0:64]); + return _signCompact(_signerPkey, digest); } // Helper functions @@ -227,36 +175,21 @@ abstract contract ImmutableSeaportTestHelper is Test { return offer; } - function _generateSip7Signature(bytes32 orderHash, address fulfiller, uint256 signerPkey, uint64 _expiration, ConsiderationItem[] memory _consideration) internal view returns (bytes memory) { - ReceivedItem[] memory consideration = _convertConsiderationToReceivedItem(_consideration); - bytes32 considerationHash = this._deriveReceivedItemsHash(consideration); - //bytes32 considerationHash = this._deriveConsiderationHash(consideration); - // TODO Use sub-standard 4 DOES NOT WORK - //bytes32[] memory orderHashes = new bytes32[](1); - //orderHashes[0] = orderHash; - // bytes memory substandard4Data = abi.encode(orderHashes); - // bytes memory context = abi.encodePacked(bytes1(0x04), substandard4Data); - - // Use sub-standard 3 - bytes memory context = abi.encodePacked(bytes1(0x03), considerationHash); - + 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 = _signOrder(signerPkey, orderHash, _expiration, context); + bytes memory signature = _signSIP7Order(signerPkey, orderHash, _expiration, context); return abi.encodePacked( - uint8(0), // SIP6 version + bytes1(0), fulfiller, _expiration, - this._convertSignatureToEIP2098(signature), + signature, context ); } - - function _convertToBytesWithoutArrayLength(bytes32[] memory _orders) internal view returns (bytes memory) { - bytes memory data = abi.encodePacked(_orders); - return this._stripArrayLength(data); - } - function _stripArrayLength(bytes calldata _data) external pure returns (bytes memory) { - return _data[32:_data.length]; - } -} \ No newline at end of file +} \ No newline at end of file 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); + } +} From c80975df2bc77c61d9dc88a106e7ddaea93ad9cb Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 31 Oct 2025 16:22:20 +1100 Subject: [PATCH 11/45] Update hardhat config to support 0.8.24 --- .../trading/seaport16/ImmutableSeaport.sol | 2 +- foundry.toml | 4 +- hardhat.config.ts | 13 + package.json | 3 +- yarn.lock | 283 +++++++++++++++++- 5 files changed, 296 insertions(+), 9 deletions(-) diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index c2d0841e..fb0d6973 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -1,7 +1,7 @@ // Copyright (c) Immutable Pty Ltd 2018 - 2023 // SPDX-License-Identifier: Apache-2 // solhint-disable compiler-version -pragma solidity ^0.8.17; +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"; diff --git a/foundry.toml b/foundry.toml index 8e6b1d92..095252d0 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,8 +1,8 @@ [profile.default] src = 'contracts' out = 'foundry-out' -# libs = ["lib", "node_modules"] -libs = ["lib"] +libs = ["lib", "node_modules"] +# libs = ["lib"] fs_permissions = [{ access = "read", path = "./foundry-out" }] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/hardhat.config.ts b/hardhat.config.ts index 2dc610a3..ff748494 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,15 @@ 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, + }, + }, + }, { version: "0.8.19", settings: { @@ -97,6 +107,9 @@ const config: HardhatUserConfig = { 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/package.json b/package.json index 85382492..0075105a 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", diff --git a/yarn.lock b/yarn.lock index d295a520..54676163 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" @@ -919,6 +932,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 +961,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 +997,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 +1179,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,7 +1300,7 @@ 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== @@ -1322,6 +1414,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 +1451,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 +1484,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 +3118,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" @@ -4287,6 +4413,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 +4691,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 +5340,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 +5395,51 @@ hardhat@^2.12.7, hardhat@^2.17.3: uuid "^8.3.2" ws "^7.4.6" +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 +6114,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" @@ -6426,11 +6617,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 +7165,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 +7519,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 +7799,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" @@ -8220,6 +8447,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 +8669,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 +8695,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 +8754,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 +8775,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 +8976,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 +10017,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 +10034,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" From ec2a134bec3e11c4480bd841ce0d79d307679898 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Mon, 3 Nov 2025 09:51:40 +1100 Subject: [PATCH 12/45] Install foundry for hardhat ci job --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db7fba87..29f746fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,10 @@ 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: Run Tests run: yarn test eslint: From 49a2a54be0495799ce66b16730daca621ea9e0ed Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Mon, 3 Nov 2025 09:55:36 +1100 Subject: [PATCH 13/45] Install forge deps --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29f746fd..94d09ebb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,8 @@ jobs: 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: From 39b7d98baec1d9a648a6e96d47add61535c12283 Mon Sep 17 00:00:00 2001 From: Peter Robinson Date: Mon, 3 Nov 2025 10:58:35 +1000 Subject: [PATCH 14/45] Do not reference node_modules for libraries --- foundry.lock | 35 +++++++++++++++++++++++++++++++++++ foundry.toml | 4 ++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 foundry.lock diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 00000000..0076d98e --- /dev/null +++ b/foundry.lock @@ -0,0 +1,35 @@ +{ + "lib/axelar-gmp-sdk-solidity": { + "rev": "3f6ae1a1d22590e1c9b6af66781adc72148ee447" + }, + "lib/forge-std": { + "rev": "1d9650e951204a0ddce9ff89c32f1997984cef4d" + }, + "lib/immutable-seaport-1.5.0+im1.3": { + "rev": "ae061dc008105dd8d05937df9ad9a676f878cbf9" + }, + "lib/immutable-seaport-1.6.0+im4": { + "rev": "e058101dbe69b403352598ed989c3afd845e9793" + }, + "lib/immutable-seaport-core-1.5.0+im1": { + "rev": "33e9030f308500b422926a1be12d7a1e4d6adc06" + }, + "lib/immutable-seaport-core-1.6.0+im2": { + "rev": "9ad91d82609e937a9ba3ef330df396b05f384e44" + }, + "lib/openzeppelin-contracts-4.9.3": { + "rev": "fd81a96f01cc42ef1c9a5399364968d0e07e9e90" + }, + "lib/openzeppelin-contracts-5.0.2": { + "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" + }, + "lib/openzeppelin-contracts-upgradeable-4.9.3": { + "rev": "3d4c0d5741b131c231e558d7a6213392ab3672a5" + }, + "lib/solidity-bits": { + "rev": "c243a888782b61542da380ac92e218c676427b50" + }, + "lib/solidity-bytes-utils": { + "rev": "6458fb2780a3092bc756e737f246be1de6d3d362" + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 095252d0..8e6b1d92 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,8 +1,8 @@ [profile.default] src = 'contracts' out = 'foundry-out' -libs = ["lib", "node_modules"] -# libs = ["lib"] +# libs = ["lib", "node_modules"] +libs = ["lib"] fs_permissions = [{ access = "read", path = "./foundry-out" }] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options From 99e3d5ced41f09cca7b77bef16424b578bcd577b Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Mon, 3 Nov 2025 13:38:07 +1100 Subject: [PATCH 15/45] Support SIP-7 Substandard 1 --- .../v3/ImmutableSignedZoneV3.sol | 42 ++++- .../zones/immutable-signed-zone/v3/README.md | 2 +- .../v3/interfaces/SIP7EventsAndErrors.sol | 6 + .../v3/ImmutableSignedZoneV3.t.sol | 154 +++++++++++++++++- .../v3/ImmutableSignedZoneV3Harness.t.sol | 8 + .../zones/immutable-signed-zone/v3/README.md | 73 +++++---- 6 files changed, 239 insertions(+), 46 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 90efb2c8..7b878b20 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -21,7 +21,7 @@ import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; * @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 3, 4 and 6. + * implementing substandards 1, 3, 4 and 6. * * 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 @@ -406,11 +406,12 @@ contract ImmutableSignedZoneV3 is * @return substandards Array of substandards supported. */ function _getSupportedSubstandards() internal pure returns (uint256[] memory substandards) { - // support substandards 3, 4 and 6 - substandards = new uint256[](3); - substandards[0] = 3; - substandards[1] = 4; - substandards[2] = 6; + // support substandards 1, 3, 4 and 6 + substandards = new uint256[](4); + substandards[0] = 1; + substandards[1] = 3; + substandards[2] = 4; + substandards[3] = 6; } /** @@ -452,6 +453,9 @@ contract ImmutableSignedZoneV3 is // Each _validateSubstandard* function returns the length of the substandard // segment (0 if the substandard was not matched). + startIndex = _validateSubstandard1(context[startIndex:], zoneParameters) + startIndex; + + if (startIndex == contextLength) return; startIndex = _validateSubstandard3(context[startIndex:], zoneParameters) + startIndex; if (startIndex == contextLength) return; @@ -465,6 +469,32 @@ contract ImmutableSignedZoneV3 is } } + /** + * @dev Validates substandard 1. + * + * @param context Bytes payload of context, 0 indexed to start of substandard segment. + * @param zoneParameters The zone parameters. + * @return Length of substandard segment. + */ + function _validateSubstandard1( + bytes calldata context, + ZoneParameters calldata zoneParameters + ) internal pure returns (uint256) { + if (uint8(context[0]) != 1) { + return 0; + } + + if (context.length < 33) { + revert InvalidExtraData("invalid substandard 1 data length", zoneParameters.orderHash); + } + + 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 diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md index f5cb3ab6..e80da093 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -1,6 +1,6 @@ # 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) 3, 4 and 6. +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 and 6. This zone is used by Immutable to enable: 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 index df579b25..5401998e 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -66,6 +66,12 @@ interface SIP7EventsAndErrors { */ error SubstandardViolation(uint256 substandardId, string reason, 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. 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 index 3a23b94b..f0bccb21 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -777,10 +777,11 @@ contract ImmutableSignedZoneV3Test is function test_getSupportedSubstandards() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); uint256[] memory supportedSubstandards = zone.exposed_getSupportedSubstandards(); - assertEq(supportedSubstandards.length, 3); - assertEq(supportedSubstandards[0], 3); - assertEq(supportedSubstandards[1], 4); - assertEq(supportedSubstandards[2], 6); + assertEq(supportedSubstandards.length, 4); + assertEq(supportedSubstandards[0], 1); + assertEq(supportedSubstandards[1], 3); + assertEq(supportedSubstandards[2], 4); + assertEq(supportedSubstandards[3], 6); } /* _deriveSignedOrderHash */ @@ -822,6 +823,36 @@ contract ImmutableSignedZoneV3Test is zone.exposed_validateSubstandards(new bytes(0), zoneParameters); } + function test_validateSubstandards_substandard1() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x01), uint256(45)); + zone.exposed_validateSubstandards(context, zoneParameters); + } + function test_validateSubstandards_substandard3() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); @@ -1025,11 +1056,12 @@ contract ImmutableSignedZoneV3Test is }); // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); + bytes memory substandard1Data = abi.encodePacked(uint256(0)); bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); bytes memory substandard4Data = abi.encode(orderHashes); bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); bytes memory context = abi.encodePacked( - bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + bytes1(0x01), substandard1Data, bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data ); zone.exposed_validateSubstandards(context, zoneParameters); @@ -1077,6 +1109,118 @@ contract ImmutableSignedZoneV3Test is zone.exposed_validateSubstandards(context, zoneParameters); } + /* _validateSubstandard1 */ + + function test_validateSubstandard1_returnsZeroLengthIfNotSubstandard1() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(hex"03", zoneParameters); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard1_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x01), bytes10(0)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidExtraData.selector, "invalid substandard 1 data length", zoneParameters.orderHash + ) + ); + zone.exposed_validateSubstandard1(context, zoneParameters); + } + + function test_validateSubstandard1_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x01), uint256(46)); + + vm.expectRevert(abi.encodeWithSelector(Substandard1Violation.selector, zoneParameters.orderHash, 45, 46)); + zone.exposed_validateSubstandard1(context, zoneParameters); + } + + function test_validateSubstandard1_returns33OnSuccess() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x01), uint256(45)); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(context, zoneParameters); + assertEq(substandardLengthResult, 33); + } + /* _validateSubstandard3 */ function test_validateSubstandard3_returnsZeroLengthIfNotSubstandard3() public { 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 index 59bccb49..778463ba 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -43,6 +43,14 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { return _validateSubstandards(context, zoneParameters); } + function exposed_validateSubstandard1(bytes calldata context, ZoneParameters calldata zoneParameters) + external + pure + returns (uint256) + { + return _validateSubstandard1(context, zoneParameters); + } + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) external pure diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index a51a4f63..dabbcbae 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -55,40 +55,45 @@ Operational function tests: 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 should revert. | No | Yes | -| `test_validateSubstandards_substandard3` | Context with substandard 3. | Yes | Yes | -| `test_validateSubstandards_substandard4` | Context with substandard 4. | Yes | Yes | -| `test_validateSubstandards_substandard6` | Context with substandard 6. | Yes | Yes | -| `test_validateSubstandards_multipleSubstandardsInCorrectOrder` | Context with multiple substandards. | Yes | Yes | -| `test_validateSubstandards_substandards3Then6` | Context with substandards 3 and 6, but not 4. | Yes | Yes | -| `test_validateSubstandards_allSubstandards` | Context with all substandards. | Yes | Yes | -| `test_validateSubstandards_revertsOnMultipleSubstandardsInIncorrectOrder` | Context with multiple substandards out of order. | No | 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_returns33OnSuccess` | Substandard 3 validation when derived hash matches expected hash. | 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_returnsLengthOfSubstandardSegmentOnSuccess` | 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_returnsLengthOfSubstandardSegmentOnSuccess` | Substandard 6 validation when derived hash matches expected hash. | 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 | +| 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 should revert. | No | Yes | +| `test_validateSubstandards_substandard1` | Context with substandard 1. | Yes | Yes | +| `test_validateSubstandards_substandard3` | Context with substandard 3. | Yes | Yes | +| `test_validateSubstandards_substandard4` | Context with substandard 4. | Yes | Yes | +| `test_validateSubstandards_substandard6` | Context with substandard 6. | Yes | Yes | +| `test_validateSubstandards_multipleSubstandardsInCorrectOrder` | Context with multiple substandards. | Yes | Yes | +| `test_validateSubstandards_substandards3Then6` | Context with substandards 3 and 6, but not 4. | Yes | Yes | +| `test_validateSubstandards_allSubstandards` | Context with all substandards. | 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_returns33OnSuccess` | Substandard 1 validation when first received item identifier matches expected identifier. | 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_returns33OnSuccess` | Substandard 3 validation when derived hash matches expected hash. | 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_returnsLengthOfSubstandardSegmentOnSuccess` | 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_returnsLengthOfSubstandardSegmentOnSuccess` | Substandard 6 validation when derived hash matches expected hash. | 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: From 0778324b23f2ebc032fd502f4ac20aa94ba406e7 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 10:05:53 +1100 Subject: [PATCH 16/45] Add creator token standards --- .gitmodules | 3 +++ foundry.lock | 6 ++++++ lib/creator-token-standards | 1 + 3 files changed, 10 insertions(+) create mode 160000 lib/creator-token-standards diff --git a/.gitmodules b/.gitmodules index 1ab05f02..87086272 100644 --- a/.gitmodules +++ b/.gitmodules @@ -33,3 +33,6 @@ [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/foundry.lock b/foundry.lock index 0076d98e..e5904b8c 100644 --- a/foundry.lock +++ b/foundry.lock @@ -2,6 +2,12 @@ "lib/axelar-gmp-sdk-solidity": { "rev": "3f6ae1a1d22590e1c9b6af66781adc72148ee447" }, + "lib/creator-token-standards": { + "tag": { + "name": "v5.0.0", + "rev": "980a63b33591d568b6e04b45f37deba05a55f787" + } + }, "lib/forge-std": { "rev": "1d9650e951204a0ddce9ff89c32f1997984cef4d" }, 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 From f004c3e43206b5d8c7523d1ff309639450ae638f Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 10:49:03 +1100 Subject: [PATCH 17/45] Conditionally perform substandard validation depending on before or after hook context --- .../v3/ImmutableSignedZoneV3.sol | 118 +++++++------ .../v3/IImmutableSignedZoneV3Harness.t.sol | 8 +- .../v3/ImmutableSignedZoneV3.t.sol | 160 ++++++++++++++---- .../v3/ImmutableSignedZoneV3Harness.t.sol | 20 +-- .../zones/immutable-signed-zone/v3/README.md | 35 ++-- 5 files changed, 228 insertions(+), 113 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 7b878b20..4ca798c1 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -247,9 +247,7 @@ contract ImmutableSignedZoneV3 is * @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. Note - * that this function does not validate the SIP-7 substandards. Validation - * of the SIP-7 substandards is performed in the validateOrder function. + * provided by the caller, before tokens have been transferred. * * @param zoneParameters The zone parameters containing data related to * the fulfilment execution. @@ -313,6 +311,9 @@ contract ImmutableSignedZoneV3 is revert InvalidFulfiller(expectedFulfiller, actualFulfiller, orderHash); } + // Validate supported substandards - before hook validation. + _validateSubstandards(context, zoneParameters, true); + // Derive the signedOrder hash. bytes32 signedOrderHash = _deriveSignedOrderHash(expectedFulfiller, expiration, orderHash, context); @@ -339,9 +340,6 @@ contract ImmutableSignedZoneV3 is * * @dev This function is called by Seaport whenever any extraData is * provided by the caller, after tokens have been transferred. - * Note that this function only validates the SIP-7 substandards as - * the final value of ZoneParameters.orderHashes is not known in the - * authorizeOrder function. * * @param zoneParameters The zone parameters containing data related to * the fulfilment execution. @@ -357,8 +355,8 @@ contract ImmutableSignedZoneV3 is // extraData bytes 93-end: context (optional, variable length). bytes calldata context = extraData[93:]; - // Validate supported substandards. - _validateSubstandards(context, zoneParameters); + // 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; @@ -441,7 +439,7 @@ contract ImmutableSignedZoneV3 is * @param context Bytes payload of context. * @param zoneParameters The zone parameters. */ - function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) internal pure { + function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) internal pure { uint256 startIndex = 0; uint256 contextLength = context.length; @@ -453,16 +451,16 @@ contract ImmutableSignedZoneV3 is // Each _validateSubstandard* function returns the length of the substandard // segment (0 if the substandard was not matched). - startIndex = _validateSubstandard1(context[startIndex:], zoneParameters) + startIndex; + startIndex = _validateSubstandard1(context[startIndex:], zoneParameters, before) + startIndex; if (startIndex == contextLength) return; - startIndex = _validateSubstandard3(context[startIndex:], zoneParameters) + startIndex; + startIndex = _validateSubstandard3(context[startIndex:], zoneParameters, before) + startIndex; if (startIndex == contextLength) return; - startIndex = _validateSubstandard4(context[startIndex:], zoneParameters) + startIndex; + startIndex = _validateSubstandard4(context[startIndex:], zoneParameters, before) + startIndex; if (startIndex == contextLength) return; - startIndex = _validateSubstandard6(context[startIndex:], zoneParameters) + startIndex; + startIndex = _validateSubstandard6(context[startIndex:], zoneParameters, before) + startIndex; if (startIndex != contextLength) { revert InvalidExtraData("invalid context, unexpected context length", zoneParameters.orderHash); @@ -478,7 +476,8 @@ contract ImmutableSignedZoneV3 is */ function _validateSubstandard1( bytes calldata context, - ZoneParameters calldata zoneParameters + ZoneParameters calldata zoneParameters, + bool before ) internal pure returns (uint256) { if (uint8(context[0]) != 1) { return 0; @@ -488,8 +487,11 @@ contract ImmutableSignedZoneV3 is revert InvalidExtraData("invalid substandard 1 data length", zoneParameters.orderHash); } - if (uint256(bytes32(context[1:33])) != zoneParameters.consideration[0].identifier) { - revert Substandard1Violation(zoneParameters.orderHash, zoneParameters.consideration[0].identifier, uint256(bytes32(context[1:33]))); + // Only perform validation in before hook. + if (before) { + 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; @@ -509,7 +511,8 @@ contract ImmutableSignedZoneV3 is */ function _validateSubstandard3( bytes calldata context, - ZoneParameters calldata zoneParameters + ZoneParameters calldata zoneParameters, + bool before ) internal pure returns (uint256) { if (uint8(context[0]) != 3) { return 0; @@ -519,8 +522,11 @@ contract ImmutableSignedZoneV3 is revert InvalidExtraData("invalid substandard 3 data length", zoneParameters.orderHash); } - if (_deriveReceivedItemsHash(zoneParameters.consideration, 1, 1) != bytes32(context[1:33])) { - revert Substandard3Violation(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; @@ -538,7 +544,8 @@ contract ImmutableSignedZoneV3 is */ function _validateSubstandard4( bytes calldata context, - ZoneParameters calldata zoneParameters + ZoneParameters calldata zoneParameters, + bool before ) internal pure returns (uint256) { if (uint8(context[0]) != 4) { return 0; @@ -551,11 +558,16 @@ contract ImmutableSignedZoneV3 is uint256 expectedOrderHashesSize = uint256(bytes32(context[33:65])); uint256 substandardIndexEnd = 64 + (expectedOrderHashesSize * 32); - 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); + // 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; @@ -572,7 +584,8 @@ contract ImmutableSignedZoneV3 is */ function _validateSubstandard6( bytes calldata context, - ZoneParameters calldata zoneParameters + ZoneParameters calldata zoneParameters, + bool before ) internal pure returns (uint256) { if (uint8(context[0]) != 6) { return 0; @@ -582,32 +595,35 @@ contract ImmutableSignedZoneV3 is revert InvalidExtraData("invalid substandard 6 data length", zoneParameters.orderHash); } - // 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 - ); + // 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; 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 index f73c5116..92a5bbc3 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol @@ -30,21 +30,21 @@ interface IImmutableSignedZoneV3Harness is ZoneInterface, SIP7Interface { bytes calldata context ) external view returns (bytes32 signedOrderHash); - function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure; - function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256); - function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256); - function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256); 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 index f0bccb21..265a9473 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -820,10 +820,18 @@ contract ImmutableSignedZoneV3Test is ) ); - zone.exposed_validateSubstandards(new bytes(0), zoneParameters); + zone.exposed_validateSubstandards(new bytes(0), zoneParameters, true); } - function test_validateSubstandards_substandard1() public { + 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); ReceivedItem[] memory receivedItems = new ReceivedItem[](1); @@ -850,10 +858,18 @@ contract ImmutableSignedZoneV3Test is }); bytes memory context = abi.encodePacked(bytes1(0x01), uint256(45)); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard3() public { + _test_validateSubstandards_substandard3(true); } - function test_validateSubstandards_substandard3() public { + function test_validateSubstandards_afterHookSubstandard3() public { + _test_validateSubstandards_substandard3(false); + } + + function _test_validateSubstandards_substandard3(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); ReceivedItem[] memory receivedItems = new ReceivedItem[](1); @@ -882,10 +898,18 @@ contract ImmutableSignedZoneV3Test is // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data); - zone.exposed_validateSubstandards(context, zoneParameters); + 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() public { + function _test_validateSubstandards_substandard4(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); bytes32[] memory orderHashes = new bytes32[](1); @@ -911,10 +935,18 @@ contract ImmutableSignedZoneV3Test is bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) ); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandard6() public { + _test_validateSubstandards_substandard6(true); } - function test_validateSubstandards_substandard6() public { + function test_validateSubstandards_afterHookSubstandard6() public { + _test_validateSubstandards_substandard6(false); + } + + function _test_validateSubstandards_substandard6(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); SpentItem[] memory spentItems = new SpentItem[](1); @@ -946,10 +978,18 @@ contract ImmutableSignedZoneV3Test is bytes32 substandard6Data = 0x6d0303fb2c992bf1970cab0fae2e4cd817df77741cee30dd7917b719a165af3e; bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), substandard6Data); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, before); } - function test_validateSubstandards_multipleSubstandardsInCorrectOrder() public { + 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); ReceivedItem[] memory receivedItems = new ReceivedItem[](1); @@ -983,10 +1023,18 @@ contract ImmutableSignedZoneV3Test is bytes memory substandard4Data = abi.encode(orderHashes); bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookSubstandards3Then6() public { + _test_validateSubstandards_substandards3Then6(true); } - function test_validateSubstandards_substandards3Then6() public { + function test_validateSubstandards_afterHookSubstandards3Then6() public { + _test_validateSubstandards_substandards3Then6(false); + } + + function _test_validateSubstandards_substandards3Then6(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); SpentItem[] memory spentItems = new SpentItem[](1); @@ -1020,10 +1068,18 @@ contract ImmutableSignedZoneV3Test is bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data, bytes1(0x06), substandard6Data); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + + function test_validateSubstandards_beforeHookAllSubstandards() public { + _test_validateSubstandards_allSubstandards(true); + } + + function test_validateSubstandards_afterHookAllSubstandards() public { + _test_validateSubstandards_allSubstandards(false); } - function test_validateSubstandards_allSubstandards() public { + function _test_validateSubstandards_allSubstandards(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); SpentItem[] memory spentItems = new SpentItem[](1); @@ -1064,7 +1120,7 @@ contract ImmutableSignedZoneV3Test is bytes1(0x01), substandard1Data, bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data ); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, before); } function test_validateSubstandards_revertsOnMultipleSubstandardsInIncorrectOrder() public { @@ -1106,7 +1162,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid context, unexpected context length", zoneParameters.orderHash ) ); - zone.exposed_validateSubstandards(context, zoneParameters); + zone.exposed_validateSubstandards(context, zoneParameters, true); } /* _validateSubstandard1 */ @@ -1127,7 +1183,7 @@ contract ImmutableSignedZoneV3Test is zoneHash: bytes32(0) }); - uint256 substandardLengthResult = zone.exposed_validateSubstandard1(hex"03", zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(hex"03", zoneParameters, true); assertEq(substandardLengthResult, 0); } @@ -1154,7 +1210,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid substandard 1 data length", zoneParameters.orderHash ) ); - zone.exposed_validateSubstandard1(context, zoneParameters); + zone.exposed_validateSubstandard1(context, zoneParameters, true); } function test_validateSubstandard1_revertsIfFirstReceivedItemIdentifierNotEqualToIdentifierInContext() public { @@ -1186,10 +1242,18 @@ contract ImmutableSignedZoneV3Test is bytes memory context = abi.encodePacked(bytes1(0x01), uint256(46)); vm.expectRevert(abi.encodeWithSelector(Substandard1Violation.selector, zoneParameters.orderHash, 45, 46)); - zone.exposed_validateSubstandard1(context, zoneParameters); + zone.exposed_validateSubstandard1(context, zoneParameters, true); } - function test_validateSubstandard1_returns33OnSuccess() public { + 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); ReceivedItem[] memory receivedItems = new ReceivedItem[](1); @@ -1217,7 +1281,7 @@ contract ImmutableSignedZoneV3Test is bytes memory context = abi.encodePacked(bytes1(0x01), uint256(45)); - uint256 substandardLengthResult = zone.exposed_validateSubstandard1(context, zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(context, zoneParameters, before); assertEq(substandardLengthResult, 33); } @@ -1239,7 +1303,7 @@ contract ImmutableSignedZoneV3Test is zoneHash: bytes32(0) }); - uint256 substandardLengthResult = zone.exposed_validateSubstandard3(hex"04", zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(hex"04", zoneParameters, true); assertEq(substandardLengthResult, 0); } @@ -1266,7 +1330,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid substandard 3 data length", zoneParameters.orderHash ) ); - zone.exposed_validateSubstandard3(context, zoneParameters); + zone.exposed_validateSubstandard3(context, zoneParameters, true); } function test_validateSubstandard3_revertsIfDerivedReceivedItemsHashNotEqualToHashInContext() public { @@ -1298,10 +1362,18 @@ contract ImmutableSignedZoneV3Test is bytes memory context = abi.encodePacked(bytes1(0x03), bytes32(0)); vm.expectRevert(abi.encodeWithSelector(Substandard3Violation.selector, zoneParameters.orderHash)); - zone.exposed_validateSubstandard3(context, zoneParameters); + 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() public { + function _test_validateSubstandard3_returns33OnSuccess(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); ReceivedItem[] memory receivedItems = new ReceivedItem[](1); @@ -1331,7 +1403,7 @@ contract ImmutableSignedZoneV3Test is bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); bytes memory context = abi.encodePacked(bytes1(0x03), substandard3Data); - uint256 substandardLengthResult = zone.exposed_validateSubstandard3(context, zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(context, zoneParameters, before); assertEq(substandardLengthResult, 33); } @@ -1353,7 +1425,7 @@ contract ImmutableSignedZoneV3Test is zoneHash: bytes32(0) }); - uint256 substandardLengthResult = zone.exposed_validateSubstandard4(hex"02", zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(hex"02", zoneParameters, true); assertEq(substandardLengthResult, 0); } @@ -1380,7 +1452,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid substandard 4 data length", zoneParameters.orderHash ) ); - zone.exposed_validateSubstandard4(context, zoneParameters); + zone.exposed_validateSubstandard4(context, zoneParameters, true); } function test_validateSubstandard4_revertsIfExpectedOrderHashesAreNotPresent() public { @@ -1415,10 +1487,18 @@ contract ImmutableSignedZoneV3Test is zoneParameters.orderHash ) ); - zone.exposed_validateSubstandard4(context, zoneParameters); + zone.exposed_validateSubstandard4(context, zoneParameters, false); } - function test_validateSubstandard4_returnsLengthOfSubstandardSegmentOnSuccess() public { + 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); bytes32[] memory orderHashes = new bytes32[](1); @@ -1439,7 +1519,7 @@ contract ImmutableSignedZoneV3Test is bytes memory context = abi.encodePacked(bytes1(0x04), abi.encode(orderHashes)); - uint256 substandardLengthResult = zone.exposed_validateSubstandard4(context, zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(context, zoneParameters, before); // bytes1 + bytes32 + bytes32 + bytes32 = 97 assertEq(substandardLengthResult, 97); } @@ -1462,7 +1542,7 @@ contract ImmutableSignedZoneV3Test is zoneHash: bytes32(0) }); - uint256 substandardLengthResult = zone.exposed_validateSubstandard6(hex"04", zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(hex"04", zoneParameters, true); assertEq(substandardLengthResult, 0); } @@ -1489,7 +1569,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid substandard 6 data length", zoneParameters.orderHash ) ); - zone.exposed_validateSubstandard6(context, zoneParameters); + zone.exposed_validateSubstandard6(context, zoneParameters, true); } function test_validateSubstandard6_revertsIfDerivedReceivedItemsHashesIsNotEqualToHashesInContext() public { @@ -1525,10 +1605,18 @@ contract ImmutableSignedZoneV3Test is vm.expectRevert( abi.encodeWithSelector(Substandard6Violation.selector, spentItems[0].amount, 100, zoneParameters.orderHash) ); - zone.exposed_validateSubstandard6(context, zoneParameters); + 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() public { + function _test_validateSubstandard6_returnsLengthOfSubstandardSegmentOnSuccess(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); SpentItem[] memory spentItems = new SpentItem[](1); @@ -1560,7 +1648,7 @@ contract ImmutableSignedZoneV3Test is bytes32 substandard6Data = 0x6d0303fb2c992bf1970cab0fae2e4cd817df77741cee30dd7917b719a165af3e; bytes memory context = abi.encodePacked(bytes1(0x06), uint256(100), substandard6Data); - uint256 substandardLengthResult = zone.exposed_validateSubstandard6(context, zoneParameters); + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(context, zoneParameters, before); // bytes1 + uint256 + bytes32 = 65 assertEq(substandardLengthResult, 65); } 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 index 778463ba..40b454c3 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -36,43 +36,43 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { return _deriveSignedOrderHash(fulfiller, expiration, orderHash, context); } - function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure { - return _validateSubstandards(context, zoneParameters); + return _validateSubstandards(context, zoneParameters, before); } - function exposed_validateSubstandard1(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard1(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256) { - return _validateSubstandard1(context, zoneParameters); + return _validateSubstandard1(context, zoneParameters, before); } - function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256) { - return _validateSubstandard3(context, zoneParameters); + return _validateSubstandard3(context, zoneParameters, before); } - function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256) { - return _validateSubstandard4(context, zoneParameters); + return _validateSubstandard4(context, zoneParameters, before); } - function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters) + function exposed_validateSubstandard6(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external pure returns (uint256) { - return _validateSubstandard6(context, zoneParameters); + return _validateSubstandard6(context, zoneParameters, before); } function exposed_deriveReceivedItemsHash( diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index dabbcbae..b04d36fe 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -62,31 +62,42 @@ Internal operational function tests: | `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 should revert. | No | Yes | -| `test_validateSubstandards_substandard1` | Context with substandard 1. | Yes | Yes | -| `test_validateSubstandards_substandard3` | Context with substandard 3. | Yes | Yes | -| `test_validateSubstandards_substandard4` | Context with substandard 4. | Yes | Yes | -| `test_validateSubstandards_substandard6` | Context with substandard 6. | Yes | Yes | -| `test_validateSubstandards_multipleSubstandardsInCorrectOrder` | Context with multiple substandards. | Yes | Yes | -| `test_validateSubstandards_substandards3Then6` | Context with substandards 3 and 6, but not 4. | Yes | Yes | -| `test_validateSubstandards_allSubstandards` | Context with all substandards. | 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_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_beforeHookAllSubstandards` | Context with all substandards in before hook. | Yes | Yes | +| `test_validateSubstandards_afterHookAllSubstandards` | Context with all 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_returns33OnSuccess` | Substandard 1 validation when first received item identifier matches expected identifier. | Yes | 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_returns33OnSuccess` | Substandard 3 validation when derived hash matches expected hash. | Yes | 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_returnsLengthOfSubstandardSegmentOnSuccess` | Substandard 4 validation when required order hashes are present. | Yes | 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_returnsLengthOfSubstandardSegmentOnSuccess` | Substandard 6 validation when derived hash matches expected hash. | Yes | 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_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 | From 24e8ebc5fae2493e1f04a139da820ac2fa676d49 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 14:19:31 +1100 Subject: [PATCH 18/45] Support SIP-7 Substandard 7 --- .../v3/ImmutableSignedZoneV3.sol | 56 ++- .../zones/immutable-signed-zone/v3/README.md | 4 +- .../v3/interfaces/SIP7EventsAndErrors.sol | 12 + remappings.txt | 2 +- .../utils/MockTransferValidator.t.sol | 133 ++++++++ .../v3/IImmutableSignedZoneV3Harness.t.sol | 7 +- .../v3/ImmutableSignedZoneV3.t.sol | 320 +++++++++++++++++- .../v3/ImmutableSignedZoneV3Harness.t.sol | 8 +- .../zones/immutable-signed-zone/v3/README.md | 14 +- 9 files changed, 538 insertions(+), 18 deletions(-) create mode 100644 test/trading/seaport16/utils/MockTransferValidator.t.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 index 4ca798c1..dfd8e885 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -4,6 +4,7 @@ // solhint-disable-next-line compiler-version pragma solidity ^0.8.20; +import {ITransferValidator} from "creator-token-standards/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"; @@ -21,7 +22,7 @@ import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; * @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 and 6. + * implementing substandards 1, 3, 4, 6 and 7. * * 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 @@ -348,7 +349,7 @@ contract ImmutableSignedZoneV3 is */ function validateOrder( ZoneParameters calldata zoneParameters - ) external pure override returns (bytes4 validOrderMagicValue) { + ) external override returns (bytes4 validOrderMagicValue) { // Put the extraData and orderHash on the stack for cheaper access. bytes calldata extraData = zoneParameters.extraData; @@ -404,12 +405,13 @@ contract ImmutableSignedZoneV3 is * @return substandards Array of substandards supported. */ function _getSupportedSubstandards() internal pure returns (uint256[] memory substandards) { - // support substandards 1, 3, 4 and 6 - substandards = new uint256[](4); + // support substandards 1, 3, 4, 6 and 7 + substandards = new uint256[](5); substandards[0] = 1; substandards[1] = 3; substandards[2] = 4; substandards[3] = 6; + substandards[4] = 7; } /** @@ -439,7 +441,7 @@ contract ImmutableSignedZoneV3 is * @param context Bytes payload of context. * @param zoneParameters The zone parameters. */ - function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) internal pure { + function _validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) internal { uint256 startIndex = 0; uint256 contextLength = context.length; @@ -462,6 +464,9 @@ contract ImmutableSignedZoneV3 is 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) { revert InvalidExtraData("invalid context, unexpected context length", zoneParameters.orderHash); } @@ -629,6 +634,47 @@ contract ImmutableSignedZoneV3 is return 65; } + 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) { + 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. + address token; + if (uint(zoneParameters.consideration[0].itemType) > 1) { + token = zoneParameters.consideration[0].token; + } 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 Derive the received items hash based on received item array. * diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md index e80da093..a334cb8c 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -1,6 +1,6 @@ # 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 and 6. +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 and 7. This zone is used by Immutable to enable: @@ -56,3 +56,5 @@ The sequence of events is as follows: 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 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 index 5401998e..c53ebae2 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -89,4 +89,16 @@ interface SIP7EventsAndErrors { * 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); } diff --git a/remappings.txt b/remappings.txt index c3fd9c9c..d050e56d 100644 --- a/remappings.txt +++ b/remappings.txt @@ -12,4 +12,4 @@ 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/ - +creator-token-standards/=lib/creator-token-standards/src/ diff --git a/test/trading/seaport16/utils/MockTransferValidator.t.sol b/test/trading/seaport16/utils/MockTransferValidator.t.sol new file mode 100644 index 00000000..78e6fb75 --- /dev/null +++ b/test/trading/seaport16/utils/MockTransferValidator.t.sol @@ -0,0 +1,133 @@ +// 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 "creator-token-standards/interfaces/ITransferValidator.sol"; + +contract MockTransferValidator is ITransferValidator { + bool private _shouldRevertApplyCollectionTransferPolicy = false; + bool private _shouldRevertValidateTransfer = false; + bool private _shouldRevertValidateTransferWithTokenId = false; + bool private _shouldRevertValidateTransferWithTokenIdAndAmount = false; + bool private _shouldRevertBeforeAuthorizedTransferWithOperatorAndTokenId = false; + bool private _shouldRevertAfterAuthorizedTransferWithTokenId = false; + bool private _shouldRevertBeforeAuthorizedTransferWithOperator = false; + bool private _shouldRevertAfterAuthorizedTransfer = false; + bool private _shouldRevertBeforeAuthorizedTransferWithTokenId = false; + bool private _shouldRevertBeforeAuthorizedTransferWithAmount = false; + bool private _shouldRevertAfterAuthorizedTransferWithAmount = false; + + function revertApplyCollectionTransferPolicy() public { + _shouldRevertApplyCollectionTransferPolicy = true; + } + + function revertValidateTransfer() public { + _shouldRevertValidateTransfer = true; + } + + function revertValidateTransferWithTokenId() public { + _shouldRevertValidateTransferWithTokenId = true; + } + + function revertValidateTransferWithTokenIdAndAmount() public { + _shouldRevertValidateTransferWithTokenIdAndAmount = true; + } + + function revertBeforeAuthorizedTransferWithOperatorAndTokenId() public { + _shouldRevertBeforeAuthorizedTransferWithOperatorAndTokenId = true; + } + + function revertAfterAuthorizedTransferWithTokenId() public { + _shouldRevertAfterAuthorizedTransferWithTokenId = true; + } + + function revertBeforeAuthorizedTransferWithOperator() public { + _shouldRevertBeforeAuthorizedTransferWithOperator = true; + } + + function revertAfterAuthorizedTransfer() public { + _shouldRevertAfterAuthorizedTransfer = true; + } + + function revertBeforeAuthorizedTransferWithTokenId() public { + _shouldRevertBeforeAuthorizedTransferWithTokenId = true; + } + + function revertBeforeAuthorizedTransferWithAmount() public { + _shouldRevertBeforeAuthorizedTransferWithAmount = true; + } + + function revertAfterAuthorizedTransferWithAmount() public { + _shouldRevertAfterAuthorizedTransferWithAmount = true; + } + + function applyCollectionTransferPolicy(address /* caller */, address /* from */, address /* to */) external view override { + if (_shouldRevertApplyCollectionTransferPolicy) { + revert MockTransferValidatorRevert("applyCollectionTransferPolicy(address caller, address from, address to)"); + } + } + + function validateTransfer(address /* caller */, address /* from */, address /* to */) external view override { + if (_shouldRevertValidateTransfer) { + revert MockTransferValidatorRevert("validateTransfer(address caller, address from, address to)"); + } + } + + function validateTransfer(address /* caller */, address /* from */, address /* to */, uint256 /* tokenId */) external view override { + if (_shouldRevertValidateTransferWithTokenId) { + 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) { + 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) { + revert MockTransferValidatorRevert("beforeAuthorizedTransfer(address operator, address token, uint256 tokenId)"); + } + } + + function afterAuthorizedTransfer(address /* token */, uint256 /* tokenId */) external view override { + if (_shouldRevertAfterAuthorizedTransferWithTokenId) { + revert MockTransferValidatorRevert("afterAuthorizedTransfer(address token, uint256 tokenId)"); + } + } + + function beforeAuthorizedTransfer(address /* operator */, address /* token */) external view override { + if (_shouldRevertBeforeAuthorizedTransferWithOperator) { + revert MockTransferValidatorRevert("beforeAuthorizedTransfer(address operator, address token)"); + } + } + + function afterAuthorizedTransfer(address /* token */) external view override { + if (_shouldRevertAfterAuthorizedTransfer) { + revert MockTransferValidatorRevert("afterAuthorizedTransfer(address token)"); + } + } + + function beforeAuthorizedTransfer(address /* token */, uint256 /* tokenId */) external view override { + if (_shouldRevertBeforeAuthorizedTransferWithTokenId) { + revert MockTransferValidatorRevert("beforeAuthorizedTransfer(address token, uint256 tokenId)"); + } + } + + function beforeAuthorizedTransferWithAmount(address /* token */, uint256 /* tokenId */, uint256 /* amount */) external view override { + if (_shouldRevertBeforeAuthorizedTransferWithAmount) { + revert MockTransferValidatorRevert("beforeAuthorizedTransferWithAmount(address token, uint256 tokenId, uint256 amount)"); + } + } + + function afterAuthorizedTransferWithAmount(address /* token */, uint256 /* tokenId */) external view override { + if (_shouldRevertAfterAuthorizedTransferWithAmount) { + revert MockTransferValidatorRevert("afterAuthorizedTransferWithAmount(address token, uint256 tokenId)"); + } + } +} + +error MockTransferValidatorRevert(string functionDefinition); 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 index 92a5bbc3..6bfc50d3 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol @@ -31,8 +31,7 @@ interface IImmutableSignedZoneV3Harness is ZoneInterface, SIP7Interface { ) external view returns (bytes32 signedOrderHash); function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) - external - pure; + external; function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external @@ -49,6 +48,10 @@ interface IImmutableSignedZoneV3Harness is ZoneInterface, SIP7Interface { pure returns (uint256); + function exposed_validateSubstandard7(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) + external + returns (uint256); + function exposed_deriveReceivedItemsHash( ReceivedItem[] calldata receivedItems, uint256 scalingFactorNumerator, 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 index 265a9473..f974a876 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -21,6 +21,7 @@ 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 @@ -777,11 +778,12 @@ contract ImmutableSignedZoneV3Test is function test_getSupportedSubstandards() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); uint256[] memory supportedSubstandards = zone.exposed_getSupportedSubstandards(); - assertEq(supportedSubstandards.length, 4); + assertEq(supportedSubstandards.length, 5); assertEq(supportedSubstandards[0], 1); assertEq(supportedSubstandards[1], 3); assertEq(supportedSubstandards[2], 4); assertEq(supportedSubstandards[3], 6); + assertEq(supportedSubstandards[4], 7); } /* _deriveSignedOrderHash */ @@ -981,6 +983,55 @@ contract ImmutableSignedZoneV3Test is 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100 + }); + spentItems[0] = spentItem; + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(222), address(transferValidator), address(0x7)); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + function test_validateSubstandards_beforeHookMultipleSubstandardsInCorrectOrder() public { _test_validateSubstandards_multipleSubstandardsInCorrectOrder(true); } @@ -1071,15 +1122,15 @@ contract ImmutableSignedZoneV3Test is zone.exposed_validateSubstandards(context, zoneParameters, before); } - function test_validateSubstandards_beforeHookAllSubstandards() public { - _test_validateSubstandards_allSubstandards(true); + function test_validateSubstandards_beforeHookManySubstandards() public { + _test_validateSubstandards_manySubstandards(true); } - function test_validateSubstandards_afterHookAllSubstandards() public { - _test_validateSubstandards_allSubstandards(false); + function test_validateSubstandards_afterHookManySubstandards() public { + _test_validateSubstandards_manySubstandards(false); } - function _test_validateSubstandards_allSubstandards(bool before) private { + function _test_validateSubstandards_manySubstandards(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); SpentItem[] memory spentItems = new SpentItem[](1); @@ -1653,6 +1704,263 @@ contract ImmutableSignedZoneV3Test is assertEq(substandardLengthResult, 65); } + /* _validateSubstandard7 */ + + function test_validateSubstandard7_returnsZeroLengthIfNotSubstandard7() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard7(hex"04", zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard7_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), bytes10(0)); + + 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); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(46), address(0x6), address(0x7)); + + 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); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 0, + amount: 50 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(0x6), address(0x7)); + + 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); + + transferValidator.revertBeforeAuthorizedTransferWithOperator(); + 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); + + transferValidator.revertAfterAuthorizedTransfer(); + 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard7(context, zoneParameters, before); + assertEq(substandardLengthResult, 73); + } + /* _deriveReceivedItemsHash */ function test_deriveReceivedItemsHash_returnsHashIfNoReceivedItems() public { 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 index 40b454c3..676caa85 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -38,7 +38,6 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters, bool before) external - pure { return _validateSubstandards(context, zoneParameters, before); } @@ -75,6 +74,13 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { 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_deriveReceivedItemsHash( ReceivedItem[] calldata receivedItems, uint256 scalingFactorNumerator, diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index b04d36fe..cb8b3e57 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -71,12 +71,14 @@ Internal operational function tests: | `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_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_beforeHookAllSubstandards` | Context with all substandards in before hook. | Yes | Yes | -| `test_validateSubstandards_afterHookAllSubstandards` | Context with all substandards 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 | @@ -98,6 +100,14 @@ Internal operational function tests: | `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 4. | 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_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 | From 6e8fcfca077905c1f146a50d98e1530bc4108303 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 14:53:48 +1100 Subject: [PATCH 19/45] Support SIP-7 Substandard 8 --- .../v3/ImmutableSignedZoneV3.sol | 54 ++- .../zones/immutable-signed-zone/v3/README.md | 3 +- .../v3/interfaces/SIP7EventsAndErrors.sol | 12 + .../utils/MockTransferValidator.t.sol | 188 +++++++++-- .../v3/IImmutableSignedZoneV3Harness.t.sol | 4 + .../v3/ImmutableSignedZoneV3.t.sol | 313 +++++++++++++++++- .../v3/ImmutableSignedZoneV3Harness.t.sol | 7 + .../zones/immutable-signed-zone/v3/README.md | 12 +- 8 files changed, 552 insertions(+), 41 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index dfd8e885..3a295d80 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -22,7 +22,7 @@ import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; * @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 and 7. + * 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 @@ -405,13 +405,14 @@ contract ImmutableSignedZoneV3 is * @return substandards Array of substandards supported. */ function _getSupportedSubstandards() internal pure returns (uint256[] memory substandards) { - // support substandards 1, 3, 4, 6 and 7 - substandards = new uint256[](5); + // 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; } /** @@ -467,6 +468,9 @@ contract ImmutableSignedZoneV3 is 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); } @@ -675,6 +679,50 @@ contract ImmutableSignedZoneV3 is return 73; } + 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. + address token; + uint256 tokenId; + if (uint(zoneParameters.consideration[0].itemType) > 1) { + token = zoneParameters.consideration[0].token; + tokenId = zoneParameters.consideration[0].identifier; + } 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. * diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md index a334cb8c..0e9ea585 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -1,6 +1,6 @@ # 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 and 7. +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: @@ -58,3 +58,4 @@ 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/interfaces/SIP7EventsAndErrors.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol index c53ebae2..a6709949 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -101,4 +101,16 @@ interface SIP7EventsAndErrors { * 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/test/trading/seaport16/utils/MockTransferValidator.t.sol b/test/trading/seaport16/utils/MockTransferValidator.t.sol index 78e6fb75..0e4389e3 100644 --- a/test/trading/seaport16/utils/MockTransferValidator.t.sol +++ b/test/trading/seaport16/utils/MockTransferValidator.t.sol @@ -8,123 +8,245 @@ import {ITransferValidator} from "creator-token-standards/interfaces/ITransferVa 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() public { + function revertApplyCollectionTransferPolicy(address caller, address from, address to) public { _shouldRevertApplyCollectionTransferPolicy = true; + _revertApplyCollectionTransferPolicyCaller = caller; + _revertApplyCollectionTransferPolicyFrom = from; + _revertApplyCollectionTransferPolicyTo = to; } - function revertValidateTransfer() public { + function revertValidateTransfer(address caller, address from, address to) public { _shouldRevertValidateTransfer = true; + _revertValidateTransferCaller = caller; + _revertValidateTransferFrom = from; + _revertValidateTransferTo = to; } - function revertValidateTransferWithTokenId() public { + function revertValidateTransferWithTokenId(address caller, address from, address to, uint256 tokenId) public { _shouldRevertValidateTransferWithTokenId = true; + _revertValidateTransferWithTokenIdCaller = caller; + _revertValidateTransferWithTokenIdFrom = from; + _revertValidateTransferWithTokenIdTo = to; + _revertValidateTransferWithTokenIdTokenId = tokenId; } - function revertValidateTransferWithTokenIdAndAmount() public { + 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() public { + function revertBeforeAuthorizedTransferWithOperatorAndTokenId(address operator, address token, uint256 tokenId) public { _shouldRevertBeforeAuthorizedTransferWithOperatorAndTokenId = true; + _revertBeforeAuthorizedTransferWithOperatorAndTokenIdOperator = operator; + _revertBeforeAuthorizedTransferWithOperatorAndTokenIdToken = token; + _revertBeforeAuthorizedTransferWithOperatorAndTokenIdTokenId = tokenId; } - function revertAfterAuthorizedTransferWithTokenId() public { + function revertAfterAuthorizedTransferWithTokenId(address token, uint256 tokenId) public { _shouldRevertAfterAuthorizedTransferWithTokenId = true; + _revertAfterAuthorizedTransferWithTokenIdToken = token; + _revertAfterAuthorizedTransferWithTokenIdTokenId = tokenId; } - function revertBeforeAuthorizedTransferWithOperator() public { + function revertBeforeAuthorizedTransferWithOperator(address operator, address token) public { _shouldRevertBeforeAuthorizedTransferWithOperator = true; + _revertBeforeAuthorizedTransferWithOperatorOperator = operator; + _revertBeforeAuthorizedTransferWithOperatorToken = token; } - function revertAfterAuthorizedTransfer() public { + function revertAfterAuthorizedTransfer(address token) public { _shouldRevertAfterAuthorizedTransfer = true; + _revertAfterAuthorizedTransferToken = token; } - function revertBeforeAuthorizedTransferWithTokenId() public { + function revertBeforeAuthorizedTransferWithTokenId(address token, uint256 tokenId) public { _shouldRevertBeforeAuthorizedTransferWithTokenId = true; + _revertBeforeAuthorizedTransferWithTokenIdToken = token; + _revertBeforeAuthorizedTransferWithTokenIdTokenId = tokenId; } - function revertBeforeAuthorizedTransferWithAmount() public { + function revertBeforeAuthorizedTransferWithAmount(address token, uint256 tokenId, uint256 amount) public { _shouldRevertBeforeAuthorizedTransferWithAmount = true; + _revertBeforeAuthorizedTransferWithAmountToken = token; + _revertBeforeAuthorizedTransferWithAmountTokenId = tokenId; + _revertBeforeAuthorizedTransferWithAmountAmount = amount; } - function revertAfterAuthorizedTransferWithAmount() public { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + function afterAuthorizedTransferWithAmount(address token, uint256 tokenId) external view override { + if ( + _shouldRevertAfterAuthorizedTransferWithAmount && + token == _revertAfterAuthorizedTransferWithAmountToken && + tokenId == _revertAfterAuthorizedTransferWithAmountTokenId + ) { revert MockTransferValidatorRevert("afterAuthorizedTransferWithAmount(address token, uint256 tokenId)"); } } 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 index 6bfc50d3..e051556e 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/IImmutableSignedZoneV3Harness.t.sol @@ -52,6 +52,10 @@ interface IImmutableSignedZoneV3Harness is ZoneInterface, SIP7Interface { 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, 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 index f974a876..cc2cda98 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -778,12 +778,13 @@ contract ImmutableSignedZoneV3Test is function test_getSupportedSubstandards() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); uint256[] memory supportedSubstandards = zone.exposed_getSupportedSubstandards(); - assertEq(supportedSubstandards.length, 5); + 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 */ @@ -1032,6 +1033,55 @@ contract ImmutableSignedZoneV3Test is 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100 + }); + spentItems[0] = spentItem; + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(222), address(transferValidator)); + + zone.exposed_validateSubstandards(context, zoneParameters, before); + } + function test_validateSubstandards_beforeHookMultipleSubstandardsInCorrectOrder() public { _test_validateSubstandards_multipleSubstandardsInCorrectOrder(true); } @@ -1863,7 +1913,7 @@ contract ImmutableSignedZoneV3Test is bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); - transferValidator.revertBeforeAuthorizedTransferWithOperator(); + transferValidator.revertBeforeAuthorizedTransferWithOperator(address(0x7), address(0x8)); vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "beforeAuthorizedTransfer(address operator, address token)")); zone.exposed_validateSubstandard7(context, zoneParameters, true); } @@ -1906,7 +1956,7 @@ contract ImmutableSignedZoneV3Test is bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); - transferValidator.revertAfterAuthorizedTransfer(); + transferValidator.revertAfterAuthorizedTransfer(address(0x8)); vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "afterAuthorizedTransfer(address token)")); zone.exposed_validateSubstandard7(context, zoneParameters, false); } @@ -1961,6 +2011,263 @@ contract ImmutableSignedZoneV3Test is assertEq(substandardLengthResult, 73); } + /* _validateSubstandard8 */ + + function test_validateSubstandard8_returnsZeroLengthIfNotSubstandard8() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard8(hex"04", zoneParameters, true); + assertEq(substandardLengthResult, 0); + } + + function test_validateSubstandard8_revertsIfContextLengthIsInvalid() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: new ReceivedItem[](0), + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), bytes10(0)); + + 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); + + 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 memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: new SpentItem[](0), + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(46), address(0x6)); + + 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); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC20, + token: address(0x2), + identifier: 0, + amount: 50 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(0x6)); + + 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(transferValidator)); + + transferValidator.revertBeforeAuthorizedTransferWithTokenId(address(0x8), 222); + 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(transferValidator)); + + transferValidator.revertAfterAuthorizedTransferWithTokenId(address(0x8), 222); + 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(); + + SpentItem[] memory spentItems = new SpentItem[](1); + SpentItem memory spentItem = SpentItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1 + }); + spentItems[0] = spentItem; + + ReceivedItem[] memory receivedItems = new ReceivedItem[](1); + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC20, + token: address(0x9), + identifier: 0, + amount: 100, + recipient: payable(address(0x3)) + }); + receivedItems[0] = receivedItem; + + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: bytes32(0), + fulfiller: address(0x2), + offerer: address(0x3), + offer: spentItems, + consideration: receivedItems, + extraData: new bytes(0), + orderHashes: new bytes32[](0), + startTime: 0, + endTime: 0, + zoneHash: bytes32(0) + }); + + bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(transferValidator)); + + uint256 substandardLengthResult = zone.exposed_validateSubstandard8(context, zoneParameters, before); + assertEq(substandardLengthResult, 53); + } + /* _deriveReceivedItemsHash */ function test_deriveReceivedItemsHash_returnsHashIfNoReceivedItems() public { 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 index 676caa85..c13cf09b 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -81,6 +81,13 @@ contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { 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, diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index cb8b3e57..5bfff6fc 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -73,6 +73,8 @@ Internal operational function tests: | `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 | @@ -100,7 +102,7 @@ Internal operational function tests: | `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 4. | 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 | @@ -108,6 +110,14 @@ Internal operational function tests: | `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 | From bc8b260e632083cdc8954a7bc9acc0b0629d112d Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 15:01:40 +1100 Subject: [PATCH 20/45] Update solidity docs --- .../v3/ImmutableSignedZoneV3.sol | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 3a295d80..64c5a116 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -441,6 +441,7 @@ contract ImmutableSignedZoneV3 is * * @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; @@ -477,10 +478,13 @@ contract ImmutableSignedZoneV3 is } /** - * @dev Validates substandard 1. + * @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( @@ -516,6 +520,7 @@ contract ImmutableSignedZoneV3 is * * @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( @@ -549,6 +554,7 @@ contract ImmutableSignedZoneV3 is * * @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( @@ -589,6 +595,7 @@ contract ImmutableSignedZoneV3 is * * @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( @@ -638,6 +645,16 @@ contract ImmutableSignedZoneV3 is 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, @@ -679,6 +696,16 @@ contract ImmutableSignedZoneV3 is 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, From bc1e40d9d929b995f5d59f41be3782614f88f30c Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 15:02:24 +1100 Subject: [PATCH 21/45] Update comment --- .../zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 64c5a116..5820fe25 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -437,7 +437,7 @@ contract ImmutableSignedZoneV3 is } /** - * @dev Validate substandards 3, 4 and 6 based on 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. From 8947d39b6e162399b5d51fef65bc94aacc436f46 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 4 Nov 2025 15:06:30 +1100 Subject: [PATCH 22/45] linting --- test/trading/seaport16/ImmutableSeaportTestHelper.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol index cece803f..0a451f6c 100644 --- a/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol +++ b/test/trading/seaport16/ImmutableSeaportTestHelper.t.sol @@ -125,7 +125,7 @@ abstract contract ImmutableSeaportTestHelper is Test, SigningTestHelper { return keccak256(receivedItemsHash); } - function _signOrder(uint256 _signerPkey, bytes32 _orderHash) internal view returns (bytes memory) { + 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!"); } From 207a61c8aa27310bc0911078cef21078e23af441 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Wed, 5 Nov 2025 15:17:53 +1100 Subject: [PATCH 23/45] Update hardhat config EVM version to cancun for solc 0.8.24 --- .gitignore | 1 + hardhat.config.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) 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/hardhat.config.ts b/hardhat.config.ts index ff748494..b0c1e0db 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -34,6 +34,7 @@ const config: HardhatUserConfig = { enabled: true, runs: 200, }, + evmVersion: "cancun", }, }, { @@ -101,6 +102,17 @@ const config: HardhatUserConfig = { }, }, }, + "contracts/trading/seaport16/ImmutableSeaport.sol": { + version: "0.8.24", + settings: { + viaIR: true, + optimizer: { + enabled: true, + runs: 10, + }, + evmVersion: "cancun", + }, + }, }, }, paths: { From 1f103f3c3ff0cc088120517fe8f303332f33951d Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Wed, 5 Nov 2025 15:23:46 +1100 Subject: [PATCH 24/45] Add foundry to publishing steps --- .github/workflows/publish.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 From 8ee5df98f2d940a0b2ac55510d295698b1730f29 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Wed, 5 Nov 2025 15:31:36 +1100 Subject: [PATCH 25/45] Added foundry to dry run publishing steps --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94d09ebb..441fa4e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,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 From 4d3881109b87c2c194b3c5fc7467aa34db9e0aa3 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 6 Nov 2025 10:40:13 +1100 Subject: [PATCH 26/45] Slither exclusions --- .../zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 5820fe25..39b77757 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -676,6 +676,7 @@ contract ImmutableSignedZoneV3 is } // 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; if (uint(zoneParameters.consideration[0].itemType) > 1) { token = zoneParameters.consideration[0].token; @@ -727,7 +728,9 @@ contract ImmutableSignedZoneV3 is } // 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; if (uint(zoneParameters.consideration[0].itemType) > 1) { token = zoneParameters.consideration[0].token; From 23f16c9b5474459c2d02d4eb366654161cc81473 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Thu, 6 Nov 2025 12:19:39 +1100 Subject: [PATCH 27/45] Update package deps for hardhat compatibility --- .../v3/ImmutableSignedZoneV3.sol | 2 +- package.json | 4 + remappings.txt | 2 +- .../utils/MockTransferValidator.t.sol | 2 +- yarn.lock | 81 ++++++++++++++++++- 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 39b77757..77b9e83b 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -4,7 +4,7 @@ // solhint-disable-next-line compiler-version pragma solidity ^0.8.20; -import {ITransferValidator} from "creator-token-standards/interfaces/ITransferValidator.sol"; +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"; diff --git a/package.json b/package.json index 0075105a..090e6a65 100644 --- a/package.json +++ b/package.json @@ -71,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", @@ -79,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 d050e56d..b837a519 100644 --- a/remappings.txt +++ b/remappings.txt @@ -12,4 +12,4 @@ 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/ -creator-token-standards/=lib/creator-token-standards/src/ +@limitbreak/creator-token-standards/=lib/creator-token-standards/ diff --git a/test/trading/seaport16/utils/MockTransferValidator.t.sol b/test/trading/seaport16/utils/MockTransferValidator.t.sol index 0e4389e3..c2d129f0 100644 --- a/test/trading/seaport16/utils/MockTransferValidator.t.sol +++ b/test/trading/seaport16/utils/MockTransferValidator.t.sol @@ -4,7 +4,7 @@ // solhint-disable-next-line compiler-version pragma solidity ^0.8.17; -import {ITransferValidator} from "creator-token-standards/interfaces/ITransferValidator.sol"; +import {ITransferValidator} from "@limitbreak/creator-token-standards/src/interfaces/ITransferValidator.sol"; contract MockTransferValidator is ITransferValidator { bool private _shouldRevertApplyCollectionTransferPolicy = false; diff --git a/yarn.lock b/yarn.lock index 54676163..037fd4af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -907,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" @@ -1305,11 +1321,21 @@ 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" @@ -3873,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" @@ -5395,7 +5426,7 @@ hardhat@^2.17.3: uuid "^8.3.2" ws "^7.4.6" -hardhat@^2.26.5: +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== @@ -6601,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== @@ -8122,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" @@ -8135,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" @@ -8143,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" From 57a845a70310ffbe3e3256571901421bd31186b9 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 7 Nov 2025 09:51:52 +1100 Subject: [PATCH 28/45] Undo commented out test --- .../seaport/ImmutableSeaportOperational.t.sol | 316 +++++++++--------- 1 file changed, 158 insertions(+), 158 deletions(-) diff --git a/test/trading/seaport/ImmutableSeaportOperational.t.sol b/test/trading/seaport/ImmutableSeaportOperational.t.sol index f06c48c2..476b3d5c 100644 --- a/test/trading/seaport/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport/ImmutableSeaportOperational.t.sol @@ -63,168 +63,168 @@ contract SellerWallet { -// contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableSeaportTestHelper { -// SellerWallet public sellerWallet; -// TestERC721 public erc721; -// uint256 public nftId; +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; -// function setUp() public override { -// super.setUp(); -// _setFulfillerAndZone(buyer, address(immutableSignedZone)); -// sellerWallet = new SellerWallet(); -// nftId = 1; -// vm.deal(buyer, 10 ether); -// } + 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 = 0xcE810B9B83082C93574784f403727369c3FE6955; + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); + immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); + } -// function testFulfillFullRestrictedOrder() public { -// _checkFulfill(OrderType.FULL_RESTRICTED); -// } + function _checkFulfill(OrderType _orderType) internal { + AdvancedOrder memory order = _prepareCheckFulfill(_orderType); -// function testFulfillPartialRestrictedOrder() public { -// _checkFulfill(OrderType.PARTIAL_RESTRICTED); -// } + // 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); -// function testRejectUnsupportedZones() public { -// // Create order with random zone -// address randomZone = makeAddr("randomZone"); -// AdvancedOrder memory order = _prepareCheckFulfill(randomZone); + // 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); + } -// 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 = 0xcE810B9B83082C93574784f403727369c3FE6955; - -// 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: 0 -// }); - -// 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 + + 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: 0 + }); + + 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 From c63cef4aaee5ae2a779ac5220b93f09add35f0b7 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 7 Nov 2025 11:44:36 +1100 Subject: [PATCH 29/45] Update foundry lockfile to track tags for all deps --- foundry.lock | 50 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/foundry.lock b/foundry.lock index e5904b8c..7623dfb9 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,6 +1,9 @@ { "lib/axelar-gmp-sdk-solidity": { - "rev": "3f6ae1a1d22590e1c9b6af66781adc72148ee447" + "tag": { + "name": "v5.8.0", + "rev": "3f6ae1a1d22590e1c9b6af66781adc72148ee447" + } }, "lib/creator-token-standards": { "tag": { @@ -12,30 +15,57 @@ "rev": "1d9650e951204a0ddce9ff89c32f1997984cef4d" }, "lib/immutable-seaport-1.5.0+im1.3": { - "rev": "ae061dc008105dd8d05937df9ad9a676f878cbf9" + "tag": { + "name": "1.5.0+im.1.3", + "rev": "ae061dc008105dd8d05937df9ad9a676f878cbf9" + } }, "lib/immutable-seaport-1.6.0+im4": { - "rev": "e058101dbe69b403352598ed989c3afd845e9793" + "tag": { + "name": "1.6.0+im4", + "rev": "e058101dbe69b403352598ed989c3afd845e9793" + } }, "lib/immutable-seaport-core-1.5.0+im1": { - "rev": "33e9030f308500b422926a1be12d7a1e4d6adc06" + "tag": { + "name": "1.5.0+im.1", + "rev": "33e9030f308500b422926a1be12d7a1e4d6adc06" + } }, "lib/immutable-seaport-core-1.6.0+im2": { - "rev": "9ad91d82609e937a9ba3ef330df396b05f384e44" + "tag": { + "name": "1.6.0+im2", + "rev": "9ad91d82609e937a9ba3ef330df396b05f384e44" + } }, "lib/openzeppelin-contracts-4.9.3": { - "rev": "fd81a96f01cc42ef1c9a5399364968d0e07e9e90" + "tag": { + "name": "v4.9.3", + "rev": "fd81a96f01cc42ef1c9a5399364968d0e07e9e90" + } }, "lib/openzeppelin-contracts-5.0.2": { - "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" + "tag": { + "name": "v5.0.2", + "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" + } }, "lib/openzeppelin-contracts-upgradeable-4.9.3": { - "rev": "3d4c0d5741b131c231e558d7a6213392ab3672a5" + "tag": { + "name": "v4.9.3", + "rev": "3d4c0d5741b131c231e558d7a6213392ab3672a5" + } }, "lib/solidity-bits": { - "rev": "c243a888782b61542da380ac92e218c676427b50" + "tag": { + "name": "v0.4.0", + "rev": "c243a888782b61542da380ac92e218c676427b50" + } }, "lib/solidity-bytes-utils": { - "rev": "6458fb2780a3092bc756e737f246be1de6d3d362" + "tag": { + "name": "v0.8.0", + "rev": "6458fb2780a3092bc756e737f246be1de6d3d362" + } } } \ No newline at end of file From eec06a03c21ee950d2bd0cefe2dac1929492e29f Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 7 Nov 2025 14:15:26 +1100 Subject: [PATCH 30/45] Update byte index comments --- .../immutable-signed-zone/v3/ImmutableSignedZoneV3.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 77b9e83b..4e18566f 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -281,14 +281,14 @@ contract ImmutableSignedZoneV3 is revert UnsupportedExtraDataVersion(uint8(extraData[0])); } - // extraData bytes 1-21: expected fulfiller. + // extraData bytes 1-20: expected fulfiller. // (zero address means not restricted). address expectedFulfiller = address(bytes20(extraData[1:21])); - // extraData bytes 21-29: expiration timestamp. + // extraData bytes 21-28: expiration timestamp. uint64 expiration = uint64(bytes8(extraData[21:29])); - // extraData bytes 29-93: signature. + // extraData bytes 29-92: signature. // (strictly requires 64 byte compact sig, ERC2098). bytes calldata signature = extraData[29:93]; From 25547d83f4e045f949ae4bf9385b60b0abb42146 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 7 Nov 2025 16:56:04 +1100 Subject: [PATCH 31/45] Refactor and add additional tests --- .../v3/ImmutableSignedZoneV3.sol | 22 +- .../v3/interfaces/SIP7EventsAndErrors.sol | 10 +- .../v3/ImmutableSignedZoneV3.t.sol | 1583 +++++++---------- .../zones/immutable-signed-zone/v3/README.md | 2 + 4 files changed, 684 insertions(+), 933 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 4e18566f..1d535b5e 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -312,6 +312,16 @@ contract ImmutableSignedZoneV3 is 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); @@ -340,7 +350,10 @@ contract ImmutableSignedZoneV3 is * @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. + * 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. @@ -354,6 +367,7 @@ contract ImmutableSignedZoneV3 is 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. @@ -502,6 +516,7 @@ contract ImmutableSignedZoneV3 is // 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]))); } @@ -670,6 +685,7 @@ contract ImmutableSignedZoneV3 is // 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]))); } @@ -678,8 +694,10 @@ contract ImmutableSignedZoneV3 is // 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 { @@ -732,9 +750,11 @@ contract ImmutableSignedZoneV3 is 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; 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 index a6709949..4221f204 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -61,10 +61,16 @@ interface SIP7EventsAndErrors { error InvalidExtraData(string reason, bytes32 orderHash); /** - * @dev Revert with an error if a substandard validation fails. + * @dev Revert with an error if a no spent items are provided. * This is a custom error that is not part of the SIP-7 spec. */ - error SubstandardViolation(uint256 substandardId, string reason, bytes32 orderHash); + error NoSpentItems(bytes32 orderHash); + + /** + * @dev Revert with an error if a 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. 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 index cc2cda98..ad2ee568 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -34,6 +34,7 @@ contract ImmutableSignedZoneV3Test is 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"); @@ -388,22 +389,12 @@ contract ImmutableSignedZoneV3Test is assertEq(documentationURI, expectedDocumentationURI); } - /* validateOrder */ + /* authorizeOrder */ function test_authorizeOrder_revertsIfEmptyExtraData() public { ImmutableSignedZoneV3 zone = _newZone(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = new bytes(0); vm.expectRevert( abi.encodeWithSelector(InvalidExtraData.selector, "extraData is empty", zoneParameters.orderHash) ); @@ -412,18 +403,8 @@ contract ImmutableSignedZoneV3Test is function test_authorizeOrder_revertsIfExtraDataLengthIsLessThan93() public { ImmutableSignedZoneV3 zone = _newZone(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: bytes(hex"01"), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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 @@ -434,50 +415,33 @@ contract ImmutableSignedZoneV3Test is function test_authorizeOrder_revertsIfExtraDataVersionIsNotSupported() public { ImmutableSignedZoneV3 zone = _newZone(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: bytes( - hex"01f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000660f3027d9ef9e6e50a74cc24433373b9cdd97693a02adcc94e562bb59a5af68190ecaea4414dcbe74618f6c77d11cbcf4a8345bbdf46e665249904925c95929ba6606638b779c6b502204fca6bb0539cdc3dc258fe3ce7b53be0c4ad620899167fedaa8" - ), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = bytes( + hex"01f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000660f3027d9ef9e6e50a74cc24433373b9cdd97693a02adcc94e562bb59a5af68190ecaea4414dcbe74618f6c77d11cbcf4a8345bbdf46e665249904925c95929ba6606638b779c6b502204fca6bb0539cdc3dc258fe3ce7b53be0c4ad620899167fedaa8" + ); vm.expectRevert(abi.encodeWithSelector(UnsupportedExtraDataVersion.selector, uint8(1))); zone.authorizeOrder(zoneParameters); } function test_authorizeOrder_revertsIfSignatureHasExpired() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - uint64 expiration = 100; - bytes memory extraData = - _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0)); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: extraData, - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); vm.expectRevert( abi.encodeWithSelector( SignatureExpired.selector, 1000, 100, - bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) + zoneParameters.orderHash ) ); // set current block.timestamp to be 1000 @@ -487,81 +451,89 @@ contract ImmutableSignedZoneV3Test is function test_authorizeOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - address randomFulfiller = makeAddr("random"); - bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - uint64 expiration = 100; - bytes memory extraData = - _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0)); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.fulfiller = makeAddr("random"); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + FULFILLER, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: randomFulfiller, - offerer: OFFERER, - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: extraData, - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); vm.expectRevert( abi.encodeWithSelector( InvalidFulfiller.selector, FULFILLER, - randomFulfiller, - bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) + zoneParameters.fulfiller, + zoneParameters.orderHash ) ); zone.authorizeOrder(zoneParameters); } - function test_authorizeOrder_revertsIfSignerIsNotActive() public { + function test_authorizeOrder_revertsIfNoSpentItems() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - // no signer added - bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - uint64 expiration = 100; + 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) + ); - SpentItem[] memory spentItems = new SpentItem[](1); - spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); + vm.expectRevert( + abi.encodeWithSelector( + NoSpentItems.selector, + zoneParameters.orderHash + ) + ); + zone.authorizeOrder(zoneParameters); + } - 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; + function test_authorizeOrder_revertsIfNoReceivedItems() public { + ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); + 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) + ); - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); - bytes memory substandard4Data = abi.encode(orderHashes); - bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); - bytes memory context = abi.encodePacked( - bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + vm.expectRevert( + abi.encodeWithSelector( + NoReceivedItems.selector, + zoneParameters.orderHash + ) ); + zone.authorizeOrder(zoneParameters); + } - bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context); + 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) + ); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: spentItems, - consideration: receivedItems, - extraData: extraData, - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); vm.expectRevert( abi.encodeWithSelector(SignerNotActive.selector, address(0x6E12D8C87503D4287c294f2Fdef96ACd9DFf6bd2)) ); @@ -576,47 +548,16 @@ contract ImmutableSignedZoneV3Test is vm.prank(OWNER); zone.addSigner(SIGNER); - bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - uint64 expiration = 100; - - SpentItem[] memory spentItems = new SpentItem[](1); - spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); - - 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; - - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); - bytes memory substandard4Data = abi.encode(orderHashes); - bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); - bytes memory context = abi.encodePacked( - bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + 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) ); - bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: spentItems, - consideration: receivedItems, - extraData: extraData, - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); assertEq(zone.authorizeOrder(zoneParameters), bytes4(0x01e4d72a)); } @@ -630,40 +571,15 @@ contract ImmutableSignedZoneV3Test is vm.prank(OWNER); zone.addSigner(SIGNER); - bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - uint64 expiration = 100; - - SpentItem[] memory spentItems = new SpentItem[](1); - spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); - - 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; - - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - bytes memory extraData = - _buildExtraDataWithoutContext(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, new bytes(0)); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: spentItems, - consideration: receivedItems, - extraData: extraData, - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + zoneParameters.extraData = _buildExtraData( + zone, + SIGNER_PRIVATE_KEY, + zoneParameters.fulfiller, + DEFAULT_EXPIRATION, + zoneParameters.orderHash, + new bytes(0) + ); vm.expectRevert( abi.encodeWithSelector( @@ -681,47 +597,16 @@ contract ImmutableSignedZoneV3Test is vm.prank(OWNER); zone.addSigner(SIGNER); - bytes32 orderHash = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - uint64 expiration = 100; - - SpentItem[] memory spentItems = new SpentItem[](1); - spentItems[0] = SpentItem({itemType: ItemType.ERC1155, token: address(0x5), identifier: 222, amount: 10}); - - 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; - - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); - bytes memory substandard4Data = abi.encode(orderHashes); - bytes memory substandard6Data = abi.encodePacked(uint256(10), substandard3Data); - bytes memory context = abi.encodePacked( - bytes1(0x03), substandard3Data, bytes1(0x04), substandard4Data, bytes1(0x06), substandard6Data + 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) ); - bytes memory extraData = _buildExtraData(zone, SIGNER_PRIVATE_KEY, FULFILLER, expiration, orderHash, context); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9), - fulfiller: FULFILLER, - offerer: OFFERER, - offer: spentItems, - consideration: receivedItems, - extraData: extraData, - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); assertEq(zone.validateOrder(zoneParameters), bytes4(0x17b1f942)); } @@ -804,18 +689,16 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandards_revertsIfEmptyContext() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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( @@ -823,7 +706,7 @@ contract ImmutableSignedZoneV3Test is ) ); - zone.exposed_validateSubstandards(new bytes(0), zoneParameters, true); + zone.exposed_validateSubstandards(context, zoneParameters, true); } function test_validateSubstandards_beforeHookSubstandard1() public { @@ -837,30 +720,17 @@ contract ImmutableSignedZoneV3Test is function _test_validateSubstandards_substandard1(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - 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 memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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 + ); - bytes memory context = abi.encodePacked(bytes1(0x01), uint256(45)); zone.exposed_validateSubstandards(context, zoneParameters, before); } @@ -875,32 +745,18 @@ contract ImmutableSignedZoneV3Test is function _test_validateSubstandards_substandard3(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - 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 memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); + 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); } @@ -915,27 +771,16 @@ contract ImmutableSignedZoneV3Test is function _test_validateSubstandards_substandard4(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - bytes memory context = abi.encodePacked( - bytes1(0x04), - bytes32(uint256(32)), - bytes32(uint256(1)), - bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9) + 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); @@ -952,8 +797,11 @@ contract ImmutableSignedZoneV3Test is 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({ @@ -963,23 +811,18 @@ contract ImmutableSignedZoneV3Test is amount: 10, recipient: payable(address(0x3)) }); + zoneParameters.consideration = receivedItems; - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 100, 10)); - bytes32 substandard6Data = 0x6d0303fb2c992bf1970cab0fae2e4cd817df77741cee30dd7917b719a165af3e; + 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); } @@ -996,6 +839,8 @@ contract ImmutableSignedZoneV3Test is 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, @@ -1004,6 +849,7 @@ contract ImmutableSignedZoneV3Test is amount: 100 }); spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); ReceivedItem memory receivedItem = ReceivedItem({ @@ -1014,21 +860,17 @@ contract ImmutableSignedZoneV3Test is recipient: payable(address(0x3)) }); receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - bytes memory context = abi.encodePacked(bytes1(0x07), uint256(222), address(transferValidator), address(0x7)); + 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); } @@ -1045,6 +887,8 @@ contract ImmutableSignedZoneV3Test is 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, @@ -1053,31 +897,28 @@ contract ImmutableSignedZoneV3Test is 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 memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) + ReceivedItem memory receivedItem = ReceivedItem({ + itemType: ItemType.ERC721, + token: address(0x8), + identifier: 222, + amount: 1, + recipient: payable(address(0x3)) }); - - bytes memory context = abi.encodePacked(bytes1(0x08), uint256(222), address(transferValidator)); + 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); } @@ -1093,36 +934,19 @@ contract ImmutableSignedZoneV3Test is function _test_validateSubstandards_multipleSubstandardsInCorrectOrder(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - 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; - - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); - bytes memory substandard4Data = abi.encode(orderHashes); + 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); } @@ -1138,8 +962,11 @@ contract ImmutableSignedZoneV3Test is 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({ @@ -1150,24 +977,19 @@ contract ImmutableSignedZoneV3Test is recipient: payable(address(0x3)) }); receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); + 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); } @@ -1183,8 +1005,11 @@ contract ImmutableSignedZoneV3Test is 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({ @@ -1195,31 +1020,23 @@ contract ImmutableSignedZoneV3Test is recipient: payable(address(0x3)) }); receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); bytes memory substandard1Data = abi.encodePacked(uint256(0)); - bytes32 substandard3Data = bytes32(0xec07a42041c18889c5c5dcd348923ea9f3d0979735bd8b3b687ebda38d9b6a31); - bytes memory substandard4Data = abi.encode(orderHashes); + 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); } @@ -1227,6 +1044,8 @@ contract ImmutableSignedZoneV3Test is 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, @@ -1236,27 +1055,19 @@ contract ImmutableSignedZoneV3Test is recipient: payable(address(0x3)) }); receivedItems[0] = receivedItem; + zoneParameters.consideration = receivedItems; - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); - bytes memory substandard4Data = abi.encode(orderHashes); + 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( @@ -1271,40 +1082,34 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard1_returnsZeroLengthIfNotSubstandard1() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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(hex"03", zoneParameters, true); + uint256 substandardLengthResult = zone.exposed_validateSubstandard1(context, zoneParameters, true); assertEq(substandardLengthResult, 0); } function test_validateSubstandard1_revertsIfContextLengthIsInvalid() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - + 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( @@ -1317,6 +1122,12 @@ contract ImmutableSignedZoneV3Test is 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, @@ -1326,21 +1137,17 @@ contract ImmutableSignedZoneV3Test is recipient: payable(address(0x3)) }); receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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); @@ -1357,6 +1164,12 @@ contract ImmutableSignedZoneV3Test is 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, @@ -1366,21 +1179,17 @@ contract ImmutableSignedZoneV3Test is recipient: payable(address(0x3)) }); receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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); @@ -1391,40 +1200,34 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard3_returnsZeroLengthIfNotSubstandard3() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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(hex"04", zoneParameters, true); + uint256 substandardLengthResult = zone.exposed_validateSubstandard3(context, zoneParameters, true); assertEq(substandardLengthResult, 0); } function test_validateSubstandard3_revertsIfContextLengthIsInvalid() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - + 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( @@ -1437,30 +1240,16 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard3_revertsIfDerivedReceivedItemsHashNotEqualToHashInContext() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - 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 memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - + 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); @@ -1477,32 +1266,18 @@ contract ImmutableSignedZoneV3Test is function _test_validateSubstandard3_returns33OnSuccess(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - 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 memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 1, 1)); - bytes32 substandard3Data = bytes32(0x7426c58179a9510d8d9f42ecb0deff6c2fdb177027f684c57f1f2795e25b433e); + 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); @@ -1513,40 +1288,35 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard4_returnsZeroLengthIfNotSubstandard4() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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(hex"02", zoneParameters, true); + uint256 substandardLengthResult = zone.exposed_validateSubstandard4(context, zoneParameters, true); assertEq(substandardLengthResult, 0); } function test_validateSubstandard4_revertsIfContextLengthIsInvalid() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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( @@ -1559,26 +1329,24 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard4_revertsIfExpectedOrderHashesAreNotPresent() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + bytes32[] memory orderHashes = new bytes32[](1); orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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( @@ -1602,23 +1370,16 @@ contract ImmutableSignedZoneV3Test is function _test_validateSubstandard4_returnsLengthOfSubstandardSegmentOnSuccess(bool before) private { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - bytes32[] memory orderHashes = new bytes32[](1); - orderHashes[0] = bytes32(0x43592598d0419e49d268e9b553427fd7ba1dd091eaa3f6127161e44afb7b40f9); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: orderHashes, - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - bytes memory context = abi.encodePacked(bytes1(0x04), abi.encode(orderHashes)); + 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 @@ -1630,40 +1391,34 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard6_returnsZeroLengthIfNotSubstandard6() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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(hex"04", zoneParameters, true); + uint256 substandardLengthResult = zone.exposed_validateSubstandard6(context, zoneParameters, true); assertEq(substandardLengthResult, 0); } function test_validateSubstandard6_revertsIfContextLengthIsInvalid() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - + 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( @@ -1676,35 +1431,19 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard6_revertsIfDerivedReceivedItemsHashesIsNotEqualToHashesInContext() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - SpentItem[] memory spentItems = new SpentItem[](1); - spentItems[0] = SpentItem({itemType: ItemType.ERC721, token: address(0x2), identifier: 222, amount: 10}); - - ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - receivedItems[0] = ReceivedItem({ - itemType: ItemType.ERC20, - token: address(0x2), - identifier: 222, - amount: 10, - recipient: payable(address(0x3)) - }); - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - + 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, spentItems[0].amount, 100, zoneParameters.orderHash) + abi.encodeWithSelector(Substandard6Violation.selector, zoneParameters.offer[0].amount, 100, zoneParameters.orderHash) ); zone.exposed_validateSubstandard6(context, zoneParameters, true); } @@ -1720,8 +1459,11 @@ contract ImmutableSignedZoneV3Test is 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({ @@ -1731,23 +1473,18 @@ contract ImmutableSignedZoneV3Test is amount: 10, recipient: payable(address(0x3)) }); + zoneParameters.consideration = receivedItems; - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); - - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, 100, 10)); - bytes32 substandard6Data = 0x6d0303fb2c992bf1970cab0fae2e4cd817df77741cee30dd7917b719a165af3e; + 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 @@ -1759,40 +1496,35 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard7_returnsZeroLengthIfNotSubstandard7() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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(hex"04", zoneParameters, true); + uint256 substandardLengthResult = zone.exposed_validateSubstandard7(context, zoneParameters, true); assertEq(substandardLengthResult, 0); } function test_validateSubstandard7_revertsIfContextLengthIsInvalid() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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( @@ -1805,30 +1537,31 @@ contract ImmutableSignedZoneV3Test is 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); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC721, token: address(0x2), identifier: 45, amount: 1, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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); @@ -1837,39 +1570,36 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard7_revertsIfItemTypeRequirementsAreNotMet() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + SpentItem[] memory spentItems = new SpentItem[](1); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC20, token: address(0x2), identifier: 0, amount: 50 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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); @@ -1878,42 +1608,40 @@ contract ImmutableSignedZoneV3Test is 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); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC721, token: address(0x8), identifier: 222, amount: 1 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + zoneParameters.consideration = receivedItems; - bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); + 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(address(0x7), address(0x8)); + transferValidator.revertBeforeAuthorizedTransferWithOperator(operator, zoneParameters.offer[0].token); vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "beforeAuthorizedTransfer(address operator, address token)")); zone.exposed_validateSubstandard7(context, zoneParameters, true); } @@ -1921,42 +1649,40 @@ contract ImmutableSignedZoneV3Test is 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); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC721, token: address(0x8), identifier: 222, amount: 1 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + zoneParameters.consideration = receivedItems; - bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); + 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(address(0x8)); + transferValidator.revertAfterAuthorizedTransfer(zoneParameters.offer[0].token); vm.expectRevert(abi.encodeWithSelector(MockTransferValidatorRevert.selector, "afterAuthorizedTransfer(address token)")); zone.exposed_validateSubstandard7(context, zoneParameters, false); } @@ -1972,40 +1698,38 @@ contract ImmutableSignedZoneV3Test is 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); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC721, token: address(0x8), identifier: 222, amount: 1 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + zoneParameters.consideration = receivedItems; - bytes memory context = abi.encodePacked(bytes1(0x07), uint256(0), address(transferValidator), address(0x7)); + 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); @@ -2016,40 +1740,36 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard8_returnsZeroLengthIfNotSubstandard8() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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(hex"04", zoneParameters, true); + uint256 substandardLengthResult = zone.exposed_validateSubstandard8(context, zoneParameters, true); assertEq(substandardLengthResult, 0); } function test_validateSubstandard8_revertsIfContextLengthIsInvalid() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: new ReceivedItem[](0), - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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( @@ -2062,30 +1782,36 @@ contract ImmutableSignedZoneV3Test is 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); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC721, - token: address(0x2), + token: address(0x5), identifier: 45, amount: 1, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: new SpentItem[](0), - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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); @@ -2094,39 +1820,36 @@ contract ImmutableSignedZoneV3Test is function test_validateSubstandard8_revertsIfItemTypeRequirementsAreNotMet() public { ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + SpentItem[] memory spentItems = new SpentItem[](1); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC20, token: address(0x2), identifier: 0, amount: 50 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + 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); @@ -2136,41 +1859,38 @@ contract ImmutableSignedZoneV3Test is ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); MockTransferValidator transferValidator = new MockTransferValidator(); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + SpentItem[] memory spentItems = new SpentItem[](1); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC721, token: address(0x8), identifier: 222, amount: 1 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + zoneParameters.consideration = receivedItems; - bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(transferValidator)); + 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), 222); + 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); } @@ -2179,41 +1899,38 @@ contract ImmutableSignedZoneV3Test is ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); MockTransferValidator transferValidator = new MockTransferValidator(); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + SpentItem[] memory spentItems = new SpentItem[](1); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC721, token: address(0x8), identifier: 222, amount: 1 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + zoneParameters.consideration = receivedItems; - bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(transferValidator)); + 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), 222); + 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); } @@ -2230,39 +1947,36 @@ contract ImmutableSignedZoneV3Test is ImmutableSignedZoneV3Harness zone = _newZoneHarness(OWNER); MockTransferValidator transferValidator = new MockTransferValidator(); + ZoneParameters memory zoneParameters = _defaultBaseZoneParameters(); + SpentItem[] memory spentItems = new SpentItem[](1); - SpentItem memory spentItem = SpentItem({ + spentItems[0] = SpentItem({ itemType: ItemType.ERC721, token: address(0x8), identifier: 222, amount: 1 }); - spentItems[0] = spentItem; + zoneParameters.offer = spentItems; ReceivedItem[] memory receivedItems = new ReceivedItem[](1); - ReceivedItem memory receivedItem = ReceivedItem({ + receivedItems[0] = ReceivedItem({ itemType: ItemType.ERC20, token: address(0x9), identifier: 0, amount: 100, recipient: payable(address(0x3)) }); - receivedItems[0] = receivedItem; - - ZoneParameters memory zoneParameters = ZoneParameters({ - orderHash: bytes32(0), - fulfiller: address(0x2), - offerer: address(0x3), - offer: spentItems, - consideration: receivedItems, - extraData: new bytes(0), - orderHashes: new bytes32[](0), - startTime: 0, - endTime: 0, - zoneHash: bytes32(0) - }); + zoneParameters.consideration = receivedItems; - bytes memory context = abi.encodePacked(bytes1(0x08), uint256(0), address(transferValidator)); + 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); @@ -2389,6 +2103,33 @@ contract ImmutableSignedZoneV3Test is ); } + 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, @@ -2407,24 +2148,6 @@ contract ImmutableSignedZoneV3Test is ); return extraData; } - - function _buildExtraDataWithoutContext( - 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)) - ); - return extraData; - } } // 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 index 5bfff6fc..933babad 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -48,6 +48,8 @@ Operational function tests: | `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_revertsIfContextIsEmpty` | Validate order with an empty context. | No | Yes | From 6bc039f426029e94d20fd5faecc0641591b801fe Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Fri, 7 Nov 2025 16:58:25 +1100 Subject: [PATCH 32/45] Update comment --- .../v3/interfaces/SIP7EventsAndErrors.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 4221f204..fcebb28b 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -61,13 +61,13 @@ interface SIP7EventsAndErrors { error InvalidExtraData(string reason, bytes32 orderHash); /** - * @dev Revert with an error if a no spent items are provided. + * @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 a no received items are provided. + * @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); From 72156d89fd4479c5464885499765fa497c536978 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 11 Nov 2025 10:45:46 +1100 Subject: [PATCH 33/45] Zone authorizeOrder and validateOrder caller restriction to Seaport --- .../v3/ImmutableSignedZoneV3.sol | 18 +++ .../v3/interfaces/SIP7EventsAndErrors.sol | 6 + .../seaport16/ImmutableSeaportBase.t.sol | 13 +- .../ImmutableSeaportOperational.t.sol | 2 +- ...utableSeaportSignedZoneV3Integration.t.sol | 10 +- .../v3/ImmutableSignedZoneV3.t.sol | 114 +++++++++++++++++- .../v3/ImmutableSignedZoneV3Harness.t.sol | 10 +- .../zones/immutable-signed-zone/v3/README.md | 4 + 8 files changed, 158 insertions(+), 19 deletions(-) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 1d535b5e..a0c66fa5 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -80,6 +80,9 @@ contract ImmutableSignedZoneV3 is 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; @@ -94,6 +97,7 @@ contract ImmutableSignedZoneV3 is * @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. @@ -102,6 +106,7 @@ contract ImmutableSignedZoneV3 is */ constructor( string memory zoneName, + address seaport, string memory apiEndpoint, string memory documentationURI, address owner @@ -112,6 +117,9 @@ contract ImmutableSignedZoneV3 is // Set name hash. _NAME_HASH = keccak256(bytes(zoneName)); + // Set the Seaport contract address. + _SEAPORT = seaport; + // Set the API endpoint. _apiEndpoint = apiEndpoint; @@ -258,6 +266,11 @@ contract ImmutableSignedZoneV3 is 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; @@ -363,6 +376,11 @@ contract ImmutableSignedZoneV3 is 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; 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 index fcebb28b..3c843dd8 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -60,6 +60,12 @@ interface SIP7EventsAndErrors { */ error InvalidExtraData(string reason, bytes32 orderHash); + /** + * @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. diff --git a/test/trading/seaport16/ImmutableSeaportBase.t.sol b/test/trading/seaport16/ImmutableSeaportBase.t.sol index dbeb1240..d47e394f 100644 --- a/test/trading/seaport16/ImmutableSeaportBase.t.sol +++ b/test/trading/seaport16/ImmutableSeaportBase.t.sol @@ -51,12 +51,6 @@ abstract contract ImmutableSeaportBaseTest is Test { (seller, sellerPkey) = makeAddrAndKey("seller"); // Deploy contracts - immutableSignedZone = new ImmutableSignedZoneV3("ImmutableSignedZone", "", "", owner); - bytes32 zoneManagerRole = immutableSignedZone.ZONE_MANAGER_ROLE(); - vm.prank(owner); - immutableSignedZone.grantRole(zoneManagerRole, zoneManager); - vm.prank(zoneManager); - immutableSignedZone.addSigner(immutableSigner); // 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)); @@ -70,6 +64,13 @@ abstract contract ImmutableSeaportBaseTest is Test { 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); diff --git a/test/trading/seaport16/ImmutableSeaportOperational.t.sol b/test/trading/seaport16/ImmutableSeaportOperational.t.sol index 47e57d57..afe94ebc 100644 --- a/test/trading/seaport16/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport16/ImmutableSeaportOperational.t.sol @@ -132,7 +132,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS // 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 = 0x7D86d2b5A73f1620093012C73B3a99781B11B2F5; + address derivedBadSigner = 0xbB5E3df75ae272CcEb6CEB0F4A4BCfd3AfE3f27D; vm.prank(buyer); vm.expectRevert(abi.encodeWithSelector(SIP7EventsAndErrors.SignerNotActive.selector, derivedBadSigner)); diff --git a/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol b/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol index dcd41dad..d2ab6fc1 100644 --- a/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol +++ b/test/trading/seaport16/ImmutableSeaportSignedZoneV3Integration.t.sol @@ -91,11 +91,15 @@ contract ImmutableSeaportSignedZoneV3IntegrationTest is Test, SigningTestHelper 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", "https://www.immutable.com", "https://www.immutable.com/docs", OWNER) + abi.encode("MyZoneName", address(seaport), "https://www.immutable.com", "https://www.immutable.com/docs", OWNER) ) ); vm.prank(OWNER); @@ -105,9 +109,7 @@ contract ImmutableSeaportSignedZoneV3IntegrationTest is Test, SigningTestHelper vm.prank(ZONE_MANAGER); zone.addSigner(SIGNER); - // seaport - ConduitController conduitController = new ConduitController(); - seaport = new ImmutableSeaportHarness(address(conduitController), OWNER); + // set allowed zone vm.prank(OWNER); seaport.setAllowedZone(address(zone), true); 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 index ad2ee568..42f210e4 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -40,6 +40,7 @@ contract ImmutableSignedZoneV3Test is 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 @@ -55,7 +56,7 @@ contract ImmutableSignedZoneV3Test is function test_contructor_grantsAdminRoleToOwner() public { address owner = makeAddr("owner"); ImmutableSignedZoneV3 zone = new ImmutableSignedZoneV3( - "MyZoneName", "https://www.immutable.com", "https://www.immutable.com/docs", owner + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", owner ); bool ownerHasAdminRole = zone.hasRole(zone.DEFAULT_ADMIN_ROLE(), owner); assertTrue(ownerHasAdminRole); @@ -65,7 +66,7 @@ contract ImmutableSignedZoneV3Test is vm.expectEmit(); emit SeaportCompatibleContractDeployed(); new ImmutableSignedZoneV3( - "MyZoneName", "https://www.immutable.com", "https://www.immutable.com/docs", makeAddr("owner") + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", makeAddr("owner") ); } @@ -342,7 +343,7 @@ contract ImmutableSignedZoneV3Test is string memory expectedDocumentationURI = "https://www.immutable.com/docs"; ImmutableSignedZoneV3Harness zone = - new ImmutableSignedZoneV3Harness(expectedZoneName, expectedApiEndpoint, expectedDocumentationURI, OWNER); + new ImmutableSignedZoneV3Harness(expectedZoneName, SEAPORT, expectedApiEndpoint, expectedDocumentationURI, OWNER); bytes32 expectedDomainSeparator = zone.exposed_deriveDomainSeparator(); uint256[] memory expectedSubstandards = zone.exposed_getSupportedSubstandards(); @@ -371,7 +372,7 @@ contract ImmutableSignedZoneV3Test is string memory expectedDocumentationURI = "https://www.immutable.com/docs"; ImmutableSignedZoneV3Harness zone = - new ImmutableSignedZoneV3Harness("MyZoneName", expectedApiEndpoint, expectedDocumentationURI, OWNER); + new ImmutableSignedZoneV3Harness("MyZoneName", SEAPORT, expectedApiEndpoint, expectedDocumentationURI, OWNER); bytes32 expectedDomainSeparator = zone.exposed_deriveDomainSeparator(); uint256[] memory expectedSubstandards = zone.exposed_getSupportedSubstandards(); @@ -391,6 +392,51 @@ contract ImmutableSignedZoneV3Test is /* 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(); @@ -398,6 +444,7 @@ contract ImmutableSignedZoneV3Test is vm.expectRevert( abi.encodeWithSelector(InvalidExtraData.selector, "extraData is empty", zoneParameters.orderHash) ); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -410,6 +457,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "extraData length must be at least 93 bytes", zoneParameters.orderHash ) ); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -420,6 +468,7 @@ contract ImmutableSignedZoneV3Test is hex"01f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000660f3027d9ef9e6e50a74cc24433373b9cdd97693a02adcc94e562bb59a5af68190ecaea4414dcbe74618f6c77d11cbcf4a8345bbdf46e665249904925c95929ba6606638b779c6b502204fca6bb0539cdc3dc258fe3ce7b53be0c4ad620899167fedaa8" ); vm.expectRevert(abi.encodeWithSelector(UnsupportedExtraDataVersion.selector, uint8(1))); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -446,6 +495,7 @@ contract ImmutableSignedZoneV3Test is ); // set current block.timestamp to be 1000 vm.warp(1000); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -471,6 +521,7 @@ contract ImmutableSignedZoneV3Test is zoneParameters.orderHash ) ); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -494,6 +545,7 @@ contract ImmutableSignedZoneV3Test is zoneParameters.orderHash ) ); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -517,6 +569,7 @@ contract ImmutableSignedZoneV3Test is zoneParameters.orderHash ) ); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -537,6 +590,7 @@ contract ImmutableSignedZoneV3Test is vm.expectRevert( abi.encodeWithSelector(SignerNotActive.selector, address(0x6E12D8C87503D4287c294f2Fdef96ACd9DFf6bd2)) ); + vm.prank(SEAPORT); zone.authorizeOrder(zoneParameters); } @@ -558,11 +612,57 @@ contract ImmutableSignedZoneV3Test is 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(); @@ -586,6 +686,7 @@ contract ImmutableSignedZoneV3Test is InvalidExtraData.selector, "invalid context, no substandards present", zoneParameters.orderHash ) ); + vm.prank(SEAPORT); zone.validateOrder(zoneParameters); } @@ -607,6 +708,7 @@ contract ImmutableSignedZoneV3Test is abi.encodePacked(bytes1(0x01), zoneParameters.consideration[0].identifier) ); + vm.prank(SEAPORT); assertEq(zone.validateOrder(zoneParameters), bytes4(0x17b1f942)); } @@ -2093,13 +2195,13 @@ contract ImmutableSignedZoneV3Test is function _newZone(address owner) private returns (ImmutableSignedZoneV3) { return new ImmutableSignedZoneV3( - "MyZoneName", "https://www.immutable.com", "https://www.immutable.com/docs", owner + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", owner ); } function _newZoneHarness(address owner) private returns (ImmutableSignedZoneV3Harness) { return new ImmutableSignedZoneV3Harness( - "MyZoneName", "https://www.immutable.com", "https://www.immutable.com/docs", owner + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", owner ); } 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 index c13cf09b..351791db 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3Harness.t.sol @@ -11,8 +11,14 @@ import {ImmutableSignedZoneV3} from // solhint-disable func-name-mixedcase contract ImmutableSignedZoneV3Harness is ImmutableSignedZoneV3 { - constructor(string memory zoneName, string memory apiEndpoint, string memory documentationURI, address owner) - ImmutableSignedZoneV3(zoneName, apiEndpoint, documentationURI, owner) + 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) { diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index 933babad..ea5f4294 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -35,6 +35,10 @@ Control function tests: | `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 | +| `test_authorizeOrder_revertsIfSeaportNotCaller` | `authorizeOrder` caller not Seaport. | No | Yes | +| `test_authorizeOrder_allowedIfSeaportIsCaller` | `authorizeOrder` caller is Seaport. | Yes | Yes | +| `test_validateOrder_revertsIfSeaportNotCaller` | `validateOrder` caller not Seaport. | No | Yes | +| `test_validateOrder_allowedIfSeaportIsCaller` | `validateOrder` caller is Seaport. | Yes | Yes | Operational function tests: From ed28bebb1774f229d1d6ebe0f6b6f87f3971d408 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 11 Nov 2025 10:49:40 +1100 Subject: [PATCH 34/45] Update test readme --- .../seaport16/zones/immutable-signed-zone/v3/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index ea5f4294..f9cc7de8 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -35,10 +35,6 @@ Control function tests: | `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 | -| `test_authorizeOrder_revertsIfSeaportNotCaller` | `authorizeOrder` caller not Seaport. | No | Yes | -| `test_authorizeOrder_allowedIfSeaportIsCaller` | `authorizeOrder` caller is Seaport. | Yes | Yes | -| `test_validateOrder_revertsIfSeaportNotCaller` | `validateOrder` caller not Seaport. | No | Yes | -| `test_validateOrder_allowedIfSeaportIsCaller` | `validateOrder` caller is Seaport. | Yes | Yes | Operational function tests: @@ -47,6 +43,8 @@ Operational function tests: | `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 | @@ -56,6 +54,8 @@ Operational function tests: | `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 | From 996c38b042ac9b70554122da5a738bfe752a8f78 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 11 Nov 2025 13:22:47 +1100 Subject: [PATCH 35/45] Seaport zero address check --- .../immutable-signed-zone/v3/ImmutableSignedZoneV3.sol | 3 +++ .../v3/interfaces/SIP7EventsAndErrors.sol | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index a0c66fa5..2e8b0253 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -118,6 +118,9 @@ contract ImmutableSignedZoneV3 is _NAME_HASH = keccak256(bytes(zoneName)); // Set the Seaport contract address. + if (seaport == address(0)) { + revert SeaportCannotBeZeroAddress(); + } _SEAPORT = seaport; // Set the API endpoint. 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 index 3c843dd8..4a6bade5 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/interfaces/SIP7EventsAndErrors.sol @@ -60,6 +60,12 @@ interface SIP7EventsAndErrors { */ 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. From 42392037918ef41ca1fcaf7c9e3a6e1feeeb267c Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 11 Nov 2025 14:39:30 +1100 Subject: [PATCH 36/45] Array index bounds check --- .../zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 2e8b0253..7f3ed8e8 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -610,6 +610,11 @@ contract ImmutableSignedZoneV3 is 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) { From f563d67c30487a32238a20957fe422b66ea35391 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 10:29:26 +1100 Subject: [PATCH 37/45] Update forge-std lib --- foundry.lock | 5 ++++- lib/forge-std | 2 +- remappings.txt | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/foundry.lock b/foundry.lock index 7623dfb9..40585dd2 100644 --- a/foundry.lock +++ b/foundry.lock @@ -12,7 +12,10 @@ } }, "lib/forge-std": { - "rev": "1d9650e951204a0ddce9ff89c32f1997984cef4d" + "tag": { + "name": "v1.11.0", + "rev": "8e40513d678f392f398620b3ef2b418648b33e89" + } }, "lib/immutable-seaport-1.5.0+im1.3": { "tag": { diff --git a/lib/forge-std b/lib/forge-std index 1d9650e9..8e40513d 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1d9650e951204a0ddce9ff89c32f1997984cef4d +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 diff --git a/remappings.txt b/remappings.txt index b837a519..e4a7b711 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ +forge-std/=lib/forge-std/src/ @openzeppelin/contracts/=lib/openzeppelin-contracts-4.9.3/contracts/ openzeppelin-contracts-5.0.2/=lib/openzeppelin-contracts-5.0.2/contracts/ openzeppelin-contracts-upgradeable-4.9.3/=lib/openzeppelin-contracts-upgradeable-4.9.3/contracts/ From addaf247f1e980994425eba759bd867847ff4d5e Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 13:47:09 +1100 Subject: [PATCH 38/45] Check seaport ownership transfer address --- contracts/trading/seaport16/ImmutableSeaport.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index fb0d6973..07a6f100 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -39,6 +39,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { * constructor to be CREATE2 / CREATE3 compatible. */ constructor(address conduitController, address owner) Consideration(conduitController) Ownable() { + require(owner != address(0), "ImmutableSeaport: owner is the zero address"); // Transfer ownership to the address specified in the constructor _transferOwnership(owner); } From e005055c2b9b64aa04fc71b902a0d9fc8e8e433f Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 13:47:52 +1100 Subject: [PATCH 39/45] Extract helper functions for zone checks --- .../trading/seaport16/ImmutableSeaport.sol | 125 +++++++++--------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index 07a6f100..3e06b04d 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -76,7 +76,52 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { } /** - * @dev Helper function to revert any fulfillment that has an invalid zone + * @dev Helper function to revert any basic order that has an invalid zone. + * + * @param parameters The basic order parameters. + */ + function _rejectBasicOrderIfZoneInvalid(BasicOrderParameters calldata parameters) internal view { + // Basic order types (modulo 4): 0 = FULL_OPEN, 1 = PARTIAL_OPEN, 2 = FULL_RESTRICTED, 3 = PARTIAL_RESTRICTED. Only restricted orders (types 2 and 3) are allowed + if (uint256(parameters.basicOrderType) % 4 != 2 && uint256(parameters.basicOrderType) % 4 != 3) { + revert OrderNotRestricted(); + } + _rejectIfZoneInvalid(parameters.zone); + } + + /** + * @dev Helper function to revert any order that has an invalid zone. + * + * @param order The order. + */ + function _rejectOrderIfZoneInvalid(Order memory order) internal view { + if ( + order.parameters.orderType != OrderType.FULL_RESTRICTED && + order.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + _rejectIfZoneInvalid(order.parameters.zone); + } + + /** + * @dev Helper function to revert any advanced order that has an invalid zone. + * + * @param advancedOrder The advanced order. + */ + function _rejectAdvancedOrderIfZoneInvalid(AdvancedOrder memory advancedOrder) internal view { + if ( + advancedOrder.parameters.orderType != OrderType.FULL_RESTRICTED && + advancedOrder.parameters.orderType != OrderType.PARTIAL_RESTRICTED + ) { + revert OrderNotRestricted(); + } + _rejectIfZoneInvalid(advancedOrder.parameters.zone); + } + + /** + * @dev Helper function to revert if the zone is not allowed. + * + * @param zone The zone to check. */ function _rejectIfZoneInvalid(address zone) internal view { if (!allowedZones[zone]) { @@ -112,12 +157,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { 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); + _rejectBasicOrderIfZoneInvalid(parameters); return super.fulfillBasicOrder(parameters); } @@ -154,12 +194,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { 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); + _rejectBasicOrderIfZoneInvalid(parameters); return super.fulfillBasicOrder_efficient_6GL6yc(parameters); } @@ -193,14 +228,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { 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); + _rejectOrderIfZoneInvalid(order); return super.fulfillOrder(order, fulfillerConduitKey); } @@ -260,14 +288,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { 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); + _rejectAdvancedOrderIfZoneInvalid(advancedOrder); return super.fulfillAdvancedOrder(advancedOrder, criteriaResolvers, fulfillerConduitKey, recipient); } @@ -347,15 +368,10 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { override returns (bool[] memory, /* availableOrders */ Execution[] memory /* executions */) { - for (uint256 i = 0; i < orders.length; i++) { + uint256 numberOfOrders = orders.length; + for (uint256 i = 0; i < numberOfOrders; 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); + _rejectOrderIfZoneInvalid(order); } return @@ -473,16 +489,10 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { override returns (bool[] memory, /* availableOrders */ Execution[] memory /* executions */) { - for (uint256 i = 0; i < advancedOrders.length; i++) { + uint256 numberOfAdvancedOrders = advancedOrders.length; + for (uint256 i = 0; i < numberOfAdvancedOrders; 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); + _rejectAdvancedOrderIfZoneInvalid(advancedOrder); } return @@ -536,15 +546,10 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { */ Fulfillment[] calldata fulfillments ) public payable virtual override returns (Execution[] memory /* executions */) { - for (uint256 i = 0; i < orders.length; i++) { + uint256 numberOfOrders = orders.length; + for (uint256 i = 0; i < numberOfOrders; 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); + _rejectOrderIfZoneInvalid(order); } return super.matchOrders(orders, fulfillments); @@ -617,16 +622,10 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { Fulfillment[] calldata fulfillments, address recipient ) public payable virtual override returns (Execution[] memory /* executions */) { - for (uint256 i = 0; i < advancedOrders.length; i++) { + uint256 numberOfAdvancedOrders = advancedOrders.length; + for (uint256 i = 0; i < numberOfAdvancedOrders; 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); + _rejectAdvancedOrderIfZoneInvalid(advancedOrder); } return super.matchAdvancedOrders(advancedOrders, criteriaResolvers, fulfillments, recipient); From 6252c2b5304dddd7cf0201c0ab4a087a64336565 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 13:49:40 +1100 Subject: [PATCH 40/45] Check zone owner is not zero address --- .../zones/immutable-signed-zone/v3/ZoneAccessControl.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol index d592d428..bf36af88 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ZoneAccessControl.sol @@ -22,6 +22,7 @@ abstract contract ZoneAccessControl is AccessControlEnumerable, ZoneAccessContro * @param owner The address to assign the DEFAULT_ADMIN_ROLE. */ constructor(address owner) { + require(owner != address(0), "ZoneAccessControl: owner is the zero address"); // Grant admin role to the specified owner. _grantRole(DEFAULT_ADMIN_ROLE, owner); } From 8e7b8ae83cdbd5e88a7152d5bc355e2f64ea69c2 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 13:56:12 +1100 Subject: [PATCH 41/45] Add order type to OrderNotRestricted error --- contracts/trading/seaport16/ImmutableSeaport.sol | 8 ++++---- test/trading/seaport/ImmutableSeaportOperational.t.sol | 2 +- test/trading/seaport16/ImmutableSeaportOperational.t.sol | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index 3e06b04d..721fc55f 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -25,7 +25,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { // solhint-disable-next-line named-parameters-mapping mapping(address => bool) public allowedZones; - error OrderNotRestricted(); + error OrderNotRestricted(uint8 orderType); error InvalidZone(address zone); /** @@ -83,7 +83,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { function _rejectBasicOrderIfZoneInvalid(BasicOrderParameters calldata parameters) internal view { // Basic order types (modulo 4): 0 = FULL_OPEN, 1 = PARTIAL_OPEN, 2 = FULL_RESTRICTED, 3 = PARTIAL_RESTRICTED. Only restricted orders (types 2 and 3) are allowed if (uint256(parameters.basicOrderType) % 4 != 2 && uint256(parameters.basicOrderType) % 4 != 3) { - revert OrderNotRestricted(); + revert OrderNotRestricted(uint8(uint256(parameters.basicOrderType) % 4)); } _rejectIfZoneInvalid(parameters.zone); } @@ -98,7 +98,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { order.parameters.orderType != OrderType.FULL_RESTRICTED && order.parameters.orderType != OrderType.PARTIAL_RESTRICTED ) { - revert OrderNotRestricted(); + revert OrderNotRestricted(uint8(order.parameters.orderType)); } _rejectIfZoneInvalid(order.parameters.zone); } @@ -113,7 +113,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { advancedOrder.parameters.orderType != OrderType.FULL_RESTRICTED && advancedOrder.parameters.orderType != OrderType.PARTIAL_RESTRICTED ) { - revert OrderNotRestricted(); + revert OrderNotRestricted(uint8(advancedOrder.parameters.orderType)); } _rejectIfZoneInvalid(advancedOrder.parameters.zone); } diff --git a/test/trading/seaport/ImmutableSeaportOperational.t.sol b/test/trading/seaport/ImmutableSeaportOperational.t.sol index 476b3d5c..d04d1147 100644 --- a/test/trading/seaport/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport/ImmutableSeaportOperational.t.sol @@ -100,7 +100,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS AdvancedOrder memory order = _prepareCheckFulfill(OrderType.FULL_OPEN); vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.OrderNotRestricted.selector)); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.OrderNotRestricted.selector, uint8(OrderType.FULL_OPEN))); immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); } diff --git a/test/trading/seaport16/ImmutableSeaportOperational.t.sol b/test/trading/seaport16/ImmutableSeaportOperational.t.sol index afe94ebc..6e12d93e 100644 --- a/test/trading/seaport16/ImmutableSeaportOperational.t.sol +++ b/test/trading/seaport16/ImmutableSeaportOperational.t.sol @@ -99,7 +99,7 @@ contract ImmutableSeaportOperationalTest is ImmutableSeaportBaseTest, ImmutableS AdvancedOrder memory order = _prepareCheckFulfill(OrderType.FULL_OPEN); vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.OrderNotRestricted.selector)); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.OrderNotRestricted.selector, uint8(OrderType.FULL_OPEN))); immutableSeaport.fulfillAdvancedOrder{value: 10 ether}(order, new CriteriaResolver[](0), conduitKey, buyer); } From a3ad8057386eb6c3101dff788c50f2627ef012b3 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 16:24:30 +1100 Subject: [PATCH 42/45] Revert when setting zero address zone or setting to already allowed value --- .../trading/seaport16/ImmutableSeaport.sol | 7 +++++++ .../seaport16/ImmutableSeaportConfig.t.sol | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index 721fc55f..b0ad6440 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -26,6 +26,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { mapping(address => bool) public allowedZones; error OrderNotRestricted(uint8 orderType); + error AllowedZoneAlreadySet(address zone); error InvalidZone(address zone); /** @@ -48,6 +49,12 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { * @dev Set the validity of a zone for use during fulfillment. */ function setAllowedZone(address zone, bool allowed) external onlyOwner { + require(zone != address(0), "ImmutableSeaport: zone is the zero address"); + + if (allowedZones[zone] == allowed) { + revert AllowedZoneAlreadySet(zone); + } + allowedZones[zone] = allowed; emit AllowedZoneSet(zone, allowed); } diff --git a/test/trading/seaport16/ImmutableSeaportConfig.t.sol b/test/trading/seaport16/ImmutableSeaportConfig.t.sol index 1d85ac14..101b2582 100644 --- a/test/trading/seaport16/ImmutableSeaportConfig.t.sol +++ b/test/trading/seaport16/ImmutableSeaportConfig.t.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.13; import {ImmutableSeaportBaseTest} from "./ImmutableSeaportBase.t.sol"; +import {ImmutableSeaport} from "../../../contracts/trading/seaport16/ImmutableSeaport.sol"; contract ImmutableSeaportConfigTest is ImmutableSeaportBaseTest { - function testEmitsAllowedZoneSetEvent() public { address zone = makeAddr("zone"); bool allowed = true; @@ -14,4 +14,21 @@ contract ImmutableSeaportConfigTest is ImmutableSeaportBaseTest { emit AllowedZoneSet(zone, allowed); immutableSeaport.setAllowedZone(zone, allowed); } + + function testRejectZeroAddressZone() public { + vm.prank(owner); + vm.expectRevert("ImmutableSeaport: zone is the zero address"); + immutableSeaport.setAllowedZone(address(0), true); + } + + function testRejectAllowedZoneAlreadySet() public { + address zone = makeAddr("zone"); + + vm.prank(owner); + immutableSeaport.setAllowedZone(zone, true); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.AllowedZoneAlreadySet.selector, zone)); + immutableSeaport.setAllowedZone(zone, true); + } } From 61a9f5f8c25a94ddab9b06d9bd87bd8902a5aa93 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 16:27:33 +1100 Subject: [PATCH 43/45] Add allowed value to AllowedZoneAlreadySet error --- contracts/trading/seaport16/ImmutableSeaport.sol | 4 ++-- test/trading/seaport16/ImmutableSeaportConfig.t.sol | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/trading/seaport16/ImmutableSeaport.sol b/contracts/trading/seaport16/ImmutableSeaport.sol index b0ad6440..a206f547 100644 --- a/contracts/trading/seaport16/ImmutableSeaport.sol +++ b/contracts/trading/seaport16/ImmutableSeaport.sol @@ -26,7 +26,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { mapping(address => bool) public allowedZones; error OrderNotRestricted(uint8 orderType); - error AllowedZoneAlreadySet(address zone); + error AllowedZoneAlreadySet(address zone, bool allowed); error InvalidZone(address zone); /** @@ -52,7 +52,7 @@ contract ImmutableSeaport is Consideration, Ownable, ImmutableSeaportEvents { require(zone != address(0), "ImmutableSeaport: zone is the zero address"); if (allowedZones[zone] == allowed) { - revert AllowedZoneAlreadySet(zone); + revert AllowedZoneAlreadySet(zone, allowed); } allowedZones[zone] = allowed; diff --git a/test/trading/seaport16/ImmutableSeaportConfig.t.sol b/test/trading/seaport16/ImmutableSeaportConfig.t.sol index 101b2582..f5ec8845 100644 --- a/test/trading/seaport16/ImmutableSeaportConfig.t.sol +++ b/test/trading/seaport16/ImmutableSeaportConfig.t.sol @@ -21,14 +21,22 @@ contract ImmutableSeaportConfigTest is ImmutableSeaportBaseTest { immutableSeaport.setAllowedZone(address(0), true); } - function testRejectAllowedZoneAlreadySet() public { + function testRejectAllowedZoneAlreadySetToTrue() public { address zone = makeAddr("zone"); vm.prank(owner); immutableSeaport.setAllowedZone(zone, true); vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.AllowedZoneAlreadySet.selector, zone)); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.AllowedZoneAlreadySet.selector, zone, true)); immutableSeaport.setAllowedZone(zone, true); } + + function testRejectAllowedZoneAlreadySetToFalse() public { + address zone = makeAddr("zone"); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(ImmutableSeaport.AllowedZoneAlreadySet.selector, zone, false)); + immutableSeaport.setAllowedZone(zone, false); + } } From a8124d8a91794a8a61bd50ad22e60816a2b69bca Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 16:32:38 +1100 Subject: [PATCH 44/45] Add test for zero address owner in zone constructor --- .../immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol | 7 +++++++ .../seaport16/zones/immutable-signed-zone/v3/README.md | 1 + 2 files changed, 8 insertions(+) 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 index 42f210e4..a9a81302 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -62,6 +62,13 @@ contract ImmutableSignedZoneV3Test is assertTrue(ownerHasAdminRole); } + function test_contructor_revertsIfOwnerIsTheZeroAddress() public { + vm.expectRevert("ZoneAccessControl: owner is the zero address"); + new ImmutableSignedZoneV3( + "MyZoneName", SEAPORT, "https://www.immutable.com", "https://www.immutable.com/docs", address(0) + ); + } + function test_contructor_emitsSeaportCompatibleContractDeployedEvent() public { vm.expectEmit(); emit SeaportCompatibleContractDeployed(); diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index f9cc7de8..ecf503a0 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -7,6 +7,7 @@ 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_revertsIfOwnerIsTheZeroAddress` | Check specified owner is not the zero address. | No | Yes | | `test_contructor_emitsSeaportCompatibleContractDeployedEvent` | Emits `SeaportCompatibleContractDeployed` event. | Yes | Yes | Control function tests: From 0d669d0fae9a15dd75e7e4bc718a3d02bd9b8c87 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Tue, 18 Nov 2025 17:41:00 +1100 Subject: [PATCH 45/45] Add functions to check zone signers --- .../v3/ImmutableSignedZoneV3.sol | 10 ++++++++++ .../v3/ImmutableSignedZoneV3.t.sol | 20 +++++++++++++++++++ .../zones/immutable-signed-zone/v3/README.md | 2 ++ 3 files changed, 32 insertions(+) diff --git a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol index 7f3ed8e8..5b5107ed 100644 --- a/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol +++ b/contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol @@ -136,6 +136,16 @@ contract ImmutableSignedZoneV3 is emit SeaportCompatibleContractDeployed(); } + /** + * @notice Check if a given signer is active. + * + * @param signer The signer address to check. + * @return True if the signer is active, false otherwise. + */ + function isActiveSigner(address signer) external view returns (bool) { + return _signers[signer].active; + } + /** * @notice Add a new signer to the zone. * 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 index a9a81302..4783a389 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.t.sol @@ -197,6 +197,26 @@ contract ImmutableSignedZoneV3Test is assertEq(managerCount, 0); } + /* isActiveSigner */ + + function test_isActiveSigner_returnsTrueIfSignerIsActive() public { + address signer = makeAddr("signer"); + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bytes32 managerRole = zone.ZONE_MANAGER_ROLE(); + vm.prank(OWNER); + zone.grantRole(managerRole, OWNER); + vm.prank(OWNER); + zone.addSigner(signer); + bool isActive = zone.isActiveSigner(signer); + assertTrue(isActive); + } + + function test_isActiveSigner_returnsFalseIfSignerIsNotActive() public { + ImmutableSignedZoneV3 zone = _newZone(OWNER); + bool isActive = zone.isActiveSigner(makeAddr("signer")); + assertFalse(isActive); + } + /* addSigner */ function test_addSigner_revertsIfCalledByNonZoneManagerRole() public { diff --git a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md index ecf503a0..4a005428 100644 --- a/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md +++ b/test/trading/seaport16/zones/immutable-signed-zone/v3/README.md @@ -24,6 +24,8 @@ Control function tests: | `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_isActiveSigner_returnsTrueIfSignerIsActive` | Check active signer. | Yes | Yes | +| `test_isActiveSigner_returnsFalseIfSignerIsNotActive` | Check inactive signer. | 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 |