diff --git a/audits/trading/202404-threat-model-immutable-signed-zone-v2.md b/audits/trading/202404-threat-model-immutable-signed-zone-v2.md new file mode 100644 index 00000000..e927f318 --- /dev/null +++ b/audits/trading/202404-threat-model-immutable-signed-zone-v2.md @@ -0,0 +1,15 @@ +# Immutable Signed Zone (v2) Threat Model + +## Introduction + +This document is a threat model for the [Immutable Signed Zone (v2)](../../contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md) contract built by Immutable. + +## Architecture + +## Attack Surfaces + +## Perceived Attackers + +## Attack Mitigation + +## Conclusion diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol index b5d92925..a49906f0 100644 --- a/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol @@ -2,17 +2,18 @@ // SPDX-License-Identifier: Apache-2 // solhint-disable-next-line compiler-version -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; -import {ZoneParameters, Schema, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {ZoneInterface} from "seaport/contracts/interfaces/ZoneInterface.sol"; +import {ZoneParameters, Schema, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {AccessControlEnumerable} from "openzeppelin-contracts-5.0.2/access/extensions/AccessControlEnumerable.sol"; +import {ECDSA} from "openzeppelin-contracts-5.0.2/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "openzeppelin-contracts-5.0.2/utils/cryptography/MessageHashUtils.sol"; +import {ERC165} from "openzeppelin-contracts-5.0.2/utils/introspection/ERC165.sol"; +import {Math} from "openzeppelin-contracts-5.0.2/utils/math/Math.sol"; import {SIP5Interface} from "./interfaces/SIP5Interface.sol"; import {SIP6Interface} from "./interfaces/SIP6Interface.sol"; import {SIP7Interface} from "./interfaces/SIP7Interface.sol"; -import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {Math} from "openzeppelin-contracts-5.0.2/utils/math/Math.sol"; /** * @title ImmutableSignedZoneV2 @@ -29,40 +30,44 @@ contract ImmutableSignedZoneV2 is SIP7Interface, AccessControlEnumerable { - /// @dev The EIP-712 digest parameters. - bytes32 internal immutable _VERSION_HASH = keccak256(bytes("2.0")); - bytes32 internal immutable _EIP_712_DOMAIN_TYPEHASH = keccak256( + /// @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", ")" ) ); - bytes32 internal immutable _SIGNED_ORDER_TYPEHASH = keccak256( + /// @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", ")" ) ); - uint256 internal immutable _CHAIN_ID = block.chainid; - bytes32 internal immutable _DOMAIN_SEPARATOR; - uint8 internal immutable _ACCEPTED_SIP6_VERSION = 0; + /// @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; - // slither-disable-start immutable-states - // solhint-disable-next-line var-name-mixedcase - bytes32 internal _NAME_HASH; - // slither-disable-end immutable-states + 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. - /// Request and response payloads are defined in SIP-7. - string private _sip7APIEndpoint; + string private _apiEndpoint; /// @dev The documentationURI. string private _documentationURI; @@ -85,7 +90,9 @@ contract ImmutableSignedZoneV2 is _NAME_HASH = keccak256(bytes(zoneName)); // Set the API endpoint. - _sip7APIEndpoint = apiEndpoint; + _apiEndpoint = apiEndpoint; + + // Set the documentation URI. _documentationURI = documentationURI; // Derive and set the domain separator. @@ -152,8 +159,20 @@ contract ImmutableSignedZoneV2 is * @param newApiEndpoint The new API endpoint. */ function updateAPIEndpoint(string calldata newApiEndpoint) external override onlyRole(DEFAULT_ADMIN_ROLE) { - // Update to the new API endpoint. - _sip7APIEndpoint = newApiEndpoint; + _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(DEFAULT_ADMIN_ROLE) + { + _documentationURI = newDocumentationURI; } /** @@ -175,7 +194,7 @@ contract ImmutableSignedZoneV2 is schemas = new Schema[](1); schemas[0].id = 7; schemas[0].metadata = - abi.encode(_domainSeparator(), _sip7APIEndpoint, _getSupportedSubstandards(), _documentationURI); + abi.encode(_domainSeparator(), _apiEndpoint, _getSupportedSubstandards(), _documentationURI); } /** @@ -198,28 +217,13 @@ contract ImmutableSignedZoneV2 is ) { domainSeparator = _domainSeparator(); - apiEndpoint = _sip7APIEndpoint; + apiEndpoint = _apiEndpoint; substandards = _getSupportedSubstandards(); documentationURI = _documentationURI; } - /** - * @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); - } - /** * @notice Validates a fulfilment execution. * @@ -227,7 +231,7 @@ contract ImmutableSignedZoneV2 is * provided by the caller. * * @param zoneParameters The zone parameters containing data related to - the fulfilment execution. + * the fulfilment execution. * @return validOrderMagicValue A magic value indicating if the order is * currently valid. */ @@ -299,7 +303,7 @@ contract ImmutableSignedZoneV2 is // Derive the EIP-712 digest using the domain separator and signedOrder // hash through openzepplin helper. - bytes32 digest = ECDSA.toTypedDataHash(_domainSeparator(), signedOrderHash); + bytes32 digest = MessageHashUtils.toTypedDataHash(_domainSeparator(), signedOrderHash); // Recover the signer address from the digest and signature. // Pass in R and VS from compact signature (ERC2098). @@ -315,6 +319,42 @@ contract ImmutableSignedZoneV2 is 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. * @@ -339,7 +379,7 @@ contract ImmutableSignedZoneV2 is */ function _deriveSignedOrderHash(address fulfiller, uint64 expiration, bytes32 orderHash, bytes calldata context) internal - view + pure returns (bytes32 signedOrderHash) { // Derive the signed order hash. @@ -570,25 +610,4 @@ contract ImmutableSignedZoneV2 is // All elements from values exist in sourceArray return true; } - - /** - * @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))); - } } diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md b/contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md new file mode 100644 index 00000000..303daab4 --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/README.md @@ -0,0 +1,45 @@ +# 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 | +| ------------------------------- | ---- | --------------- | -------------- | +| Not audited and no threat model | - | - | - | + +## 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[ImmutableSignedZoneV2.sol] +``` + +The sequence of events is as follows: + +1. The client makes a HTTP `POST .../fulfillment-data` request to the Immutable Orderbook, which will construct signs and sign an `extraData` payload to return to the client +2. The client calls `fulfillAdvancedOrder` or `fulfillAvailableAdavancedOrders` 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 + 1. `ImmutableSignedZoneV2.sol` validates the fulfilment execution details using the `extraData` payload, reverting if expectations are not met \ No newline at end of file diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol index c09fad68..5e35d36e 100644 --- a/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol @@ -19,9 +19,9 @@ interface SIP7Interface is SIP7EventsAndErrors { * @dev The struct for storing signer info. */ struct SignerInfo { - /// If the signer is currently active. + /// @dev If the signer is currently active. bool active; - /// If the signer has been active before. + /// @dev If the signer has been active before. bool previouslyActive; } @@ -46,6 +46,13 @@ interface SIP7Interface is SIP7EventsAndErrors { */ 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. * diff --git a/test/trading/seaport/ImmutableSeaportHarness.t.sol b/test/trading/seaport/ImmutableSeaportHarness.t.sol index 15de0c88..9f1cda4c 100644 --- a/test/trading/seaport/ImmutableSeaportHarness.t.sol +++ b/test/trading/seaport/ImmutableSeaportHarness.t.sol @@ -11,6 +11,10 @@ import {ImmutableSeaport} from "../../../contracts/trading/seaport/ImmutableSeap 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 @@ -18,10 +22,6 @@ contract ImmutableSeaportHarness is ImmutableSeaport { { return _deriveEIP712Digest(domainSeparator, orderHash); } - - function exposed_domainSeparator() external view returns (bytes32) { - return _domainSeparator(); - } } // solhint-enable func-name-mixedcase diff --git a/test/trading/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol b/test/trading/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol index 6494fc8b..aa65560c 100644 --- a/test/trading/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol +++ b/test/trading/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol @@ -6,13 +6,9 @@ pragma solidity ^0.8.17; // solhint-disable-next-line no-global-import import "forge-std/Test.sol"; -import {IImmutableSignedZoneV2Harness} from "./zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol"; -import {ConduitController} from "../../../contracts/trading/seaport/conduit/ConduitController.sol"; -import {ImmutableSeaportHarness} from "./ImmutableSeaportHarness.t.sol"; -import {SigningTestHelper} from "./utils/SigningTestHelper.t.sol"; -import {IImmutableERC1155} from "./utils/IImmutableERC1155.t.sol"; -import {IImmutableERC721} from "./utils/IImmutableERC721.t.sol"; -import {IOperatorAllowlistUpgradeable} from "./utils/IOperatorAllowlistUpgradeable.t.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/src/lib/ConsiderationEnums.sol"; import { AdvancedOrder, ConsiderationItem, @@ -22,40 +18,44 @@ import { OrderParameters, ReceivedItem } from "seaport-types/src/lib/ConsiderationStructs.sol"; -import {ItemType, OrderType} from "seaport-types/src/lib/ConsiderationEnums.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ConduitController} from "../../../contracts/trading/seaport/conduit/ConduitController.sol"; +import {ImmutableSeaportHarness} from "./ImmutableSeaportHarness.t.sol"; +import {IImmutableERC1155} from "./utils/IImmutableERC1155.t.sol"; +import {IImmutableERC721} from "./utils/IImmutableERC721.t.sol"; +import {IOperatorAllowlistUpgradeable} from "./utils/IOperatorAllowlistUpgradeable.t.sol"; +import {SigningTestHelper} from "./utils/SigningTestHelper.t.sol"; +import {IImmutableSignedZoneV2Harness} from "./zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol"; // solhint-disable func-name-mixedcase, private-vars-leading-underscore contract ImmutableSeaportSignedZoneV2IntegrationTest 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 internal constant OPERATOR_ALLOWLIST_ARTIFACT = + string private constant OPERATOR_ALLOWLIST_ARTIFACT = "./foundry-out/OperatorAllowlistUpgradeable.sol/OperatorAllowlistUpgradeable.json"; - string internal constant ERC1155_ARTIFACT = "./foundry-out/ImmutableERC1155.sol/ImmutableERC1155.json"; - string internal constant ERC20_ARTIFACT = + string private constant ERC1155_ARTIFACT = "./foundry-out/ImmutableERC1155.sol/ImmutableERC1155.json"; + string private constant ERC20_ARTIFACT = "./foundry-out/ImmutableERC20FixedSupplyNoBurn.sol/ImmutableERC20FixedSupplyNoBurn.json"; - string internal constant ERC721_ARTIFACT = "./foundry-out/ImmutableERC721.sol/ImmutableERC721.json"; - string internal constant ZONE_ARTIFACT = + string private constant ERC721_ARTIFACT = "./foundry-out/ImmutableERC721.sol/ImmutableERC721.json"; + string private constant ZONE_ARTIFACT = "./foundry-out/ImmutableSignedZoneV2Harness.t.sol/ImmutableSignedZoneV2Harness.json"; - address internal immutable OWNER = makeAddr("owner"); - address internal immutable SIGNER; - uint256 internal immutable SIGNER_PRIVATE_KEY; - address internal immutable FULFILLER = makeAddr("fulfiller"); - address internal immutable FULFILLER_TWO = makeAddr("fulfiller_two"); - address internal immutable OFFERER; - uint256 internal immutable OFFERER_PRIVATE_KEY; - address internal immutable PROTOCOL_FEE_RECEIVER = makeAddr("protocol_fee_receiver"); - address internal immutable ROYALTY_FEE_RECEIVER = makeAddr("royalty_fee_receiver"); - address internal immutable ECOSYSTEM_FEE_RECEIVER = makeAddr("ecosystem_fee_receiver"); - - ImmutableSeaportHarness internal seaport; - IImmutableSignedZoneV2Harness internal zone; - IERC20 internal erc20Token; - IImmutableERC1155 internal erc1155Token; - IImmutableERC721 internal erc721Token; + address private immutable OWNER = makeAddr("owner"); + 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; + IImmutableSignedZoneV2Harness private zone; + IERC20 private erc20Token; + IImmutableERC1155 private erc1155Token; + IImmutableERC721 private erc721Token; constructor() { (SIGNER, SIGNER_PRIVATE_KEY) = makeAddrAndKey("signer"); diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v1/README.md b/test/trading/seaport/zones/immutable-signed-zone/v1/README.md similarity index 100% rename from contracts/trading/seaport/zones/immutable-signed-zone/v1/README.md rename to test/trading/seaport/zones/immutable-signed-zone/v1/README.md diff --git a/test/trading/seaport/zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol index 5b4dcd03..8a2fca35 100644 --- a/test/trading/seaport/zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol @@ -12,6 +12,10 @@ import {SIP7Interface} from // solhint-disable func-name-mixedcase interface IImmutableSignedZoneV2Harness is ZoneInterface, SIP7Interface { + 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( @@ -50,10 +54,6 @@ interface IImmutableSignedZoneV2Harness is ZoneInterface, SIP7Interface { external pure returns (bool); - - function exposed_domainSeparator() external view returns (bytes32); - - function exposed_deriveDomainSeparator() external view returns (bytes32 domainSeparator); } // solhint-enable func-name-mixedcase 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 a822d45d..0dd27a8d 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 @@ -6,19 +6,19 @@ pragma solidity ^0.8.17; // solhint-disable-next-line no-global-import import "forge-std/Test.sol"; -import {ReceivedItem, Schema, SpentItem, ZoneParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {ReceivedItem, Schema, SpentItem, ZoneParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {ImmutableSignedZoneV2} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol"; -import {ImmutableSignedZoneV2Harness} from "./ImmutableSignedZoneV2Harness.t.sol"; -import {SigningTestHelper} from "../../../utils/SigningTestHelper.t.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {SIP5EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP5EventsAndErrors.sol"; import {SIP6EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP6EventsAndErrors.sol"; import {SIP7EventsAndErrors} from "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7EventsAndErrors.sol"; +import {SigningTestHelper} from "../../../utils/SigningTestHelper.t.sol"; +import {ImmutableSignedZoneV2Harness} from "./ImmutableSignedZoneV2Harness.t.sol"; // solhint-disable func-name-mixedcase @@ -29,16 +29,17 @@ contract ImmutableSignedZoneV2Test is SIP6EventsAndErrors, SIP7EventsAndErrors { - uint256 public constant MAX_UINT_TYPE = type(uint256).max; - // solhint-disable private-vars-leading-underscore - address internal immutable OWNER = makeAddr("owner"); - address internal immutable FULFILLER = makeAddr("fulfiller"); - address internal immutable OFFERER = makeAddr("offerer"); - address internal immutable SIGNER; - uint256 internal immutable SIGNER_PRIVATE_KEY; + 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); + constructor() { (SIGNER, SIGNER_PRIVATE_KEY) = makeAddrAndKey("signer"); } @@ -72,11 +73,16 @@ contract ImmutableSignedZoneV2Test is function test_addSigner_revertsIfCalledByNonAdminRole() public { ImmutableSignedZoneV2 zone = _newZone(OWNER); + address nonAdminAccount = makeAddr("non_admin"); vm.expectRevert( - "AccessControl: account 0x42a3d6e125aad539ac15ed04e1478eb0a4dc1489 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" + abi.encodeWithSelector( + AccessControlUnauthorizedAccount.selector, + nonAdminAccount, + zone.DEFAULT_ADMIN_ROLE() + ) ); - vm.prank(makeAddr("random")); - zone.addSigner(makeAddr("signerToAdd")); + vm.prank(nonAdminAccount); + zone.addSigner(makeAddr("signer_to_add")); } function test_addSigner_revertsIfSignerIsTheZeroAddress() public { @@ -87,7 +93,7 @@ contract ImmutableSignedZoneV2Test is } function test_addSigner_emitsSignerAddedEvent() public { - address signerToAdd = makeAddr("signerToAdd"); + address signerToAdd = makeAddr("signer_to_add"); ImmutableSignedZoneV2 zone = _newZone(OWNER); vm.expectEmit(address(zone)); emit SignerAdded(signerToAdd); @@ -96,7 +102,7 @@ contract ImmutableSignedZoneV2Test is } function test_addSigner_revertsIfSignerAlreadyActive() public { - address signerToAdd = makeAddr("signerToAdd"); + address signerToAdd = makeAddr("signer_to_add"); ImmutableSignedZoneV2 zone = _newZone(OWNER); vm.prank(OWNER); zone.addSigner(signerToAdd); @@ -106,7 +112,7 @@ contract ImmutableSignedZoneV2Test is } function test_addSigner_revertsIfSignerWasPreviouslyActive() public { - address signerToAdd = makeAddr("signerToAdd"); + address signerToAdd = makeAddr("signer_to_add"); ImmutableSignedZoneV2 zone = _newZone(OWNER); vm.prank(OWNER); zone.addSigner(signerToAdd); @@ -121,15 +127,20 @@ contract ImmutableSignedZoneV2Test is function test_removeSigner_revertsIfCalledByNonAdminRole() public { ImmutableSignedZoneV2 zone = _newZone(OWNER); + address nonAdminAccount = makeAddr("non_admin"); vm.expectRevert( - "AccessControl: account 0x42a3d6e125aad539ac15ed04e1478eb0a4dc1489 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" + abi.encodeWithSelector( + AccessControlUnauthorizedAccount.selector, + nonAdminAccount, + zone.DEFAULT_ADMIN_ROLE() + ) ); - vm.prank(makeAddr("random")); - zone.removeSigner(makeAddr("signerToRemove")); + vm.prank(nonAdminAccount); + zone.removeSigner(makeAddr("signer_to_remove")); } function test_removeSigner_revertsIfSignerNotActive() public { - address signerToRemove = makeAddr("signerToRemove"); + address signerToRemove = makeAddr("signer_to_remove"); ImmutableSignedZoneV2 zone = _newZone(OWNER); vm.expectRevert(abi.encodeWithSelector(SignerNotActive.selector, signerToRemove)); vm.prank(OWNER); @@ -137,7 +148,7 @@ contract ImmutableSignedZoneV2Test is } function test_removeSigner_emitsSignerRemovedEvent() public { - address signerToRemove = makeAddr("signerToRemove"); + address signerToRemove = makeAddr("signer_to_remove"); ImmutableSignedZoneV2 zone = _newZone(OWNER); vm.prank(OWNER); zone.addSigner(signerToRemove); @@ -151,23 +162,54 @@ contract ImmutableSignedZoneV2Test is function test_updateAPIEndpoint_revertsIfCalledByNonAdminRole() public { ImmutableSignedZoneV2 zone = _newZone(OWNER); - vm.prank(makeAddr("random")); + address nonAdminAccount = makeAddr("non_admin"); vm.expectRevert( - "AccessControl: account 0x42a3d6e125aad539ac15ed04e1478eb0a4dc1489 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" + abi.encodeWithSelector( + AccessControlUnauthorizedAccount.selector, + nonAdminAccount, + zone.DEFAULT_ADMIN_ROLE() + ) ); + vm.prank(nonAdminAccount); zone.updateAPIEndpoint("https://www.new-immutable.com"); } function test_updateAPIEndpoint_updatesAPIEndpointIfCalledByAdminRole() public { ImmutableSignedZoneV2 zone = _newZone(OWNER); - vm.prank(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_revertsIfCalledByNonAdminRole() public { + ImmutableSignedZoneV2 zone = _newZone(OWNER); + address nonAdminAccount = makeAddr("non_admin"); + vm.expectRevert( + abi.encodeWithSelector( + AccessControlUnauthorizedAccount.selector, + nonAdminAccount, + zone.DEFAULT_ADMIN_ROLE() + ) + ); + vm.prank(nonAdminAccount); + zone.updateDocumentationURI("https://www.new-immutable.com/docs"); + } + + function test_updateDocumentationURI_updatesDocumentationURIIfCalledByAdminRole() public { + ImmutableSignedZoneV2 zone = _newZone(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 { @@ -231,17 +273,6 @@ contract ImmutableSignedZoneV2Test is assertEq(documentationURI, expectedDocumentationURI); } - /* supportsInterface */ - - function test_supportsInterface() public { - ImmutableSignedZoneV2 zone = _newZone(OWNER); - assertTrue(zone.supportsInterface(0x01ffc9a7)); // ERC165 interface - assertFalse(zone.supportsInterface(0xffffffff)); // ERC165 compliance - assertTrue(zone.supportsInterface(0x3839be19)); // ZoneInterface - assertTrue(zone.supportsInterface(0x2e778efc)); // SIP-5 interface - assertTrue(zone.supportsInterface(0x1a511c70)); // SIP-7 interface - } - /* validateOrder */ function test_validateOrder_revertsIfEmptyExtraData() public { @@ -446,6 +477,45 @@ contract ImmutableSignedZoneV2Test is assertEq(zone.validateOrder(zoneParameters), bytes4(0x17b1f942)); } + /* supportsInterface */ + + function test_supportsInterface() public { + ImmutableSignedZoneV2 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 { + ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); + + bytes32 domainSeparator = zone.exposed_domainSeparator(); + assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + } + + function test_domainSeparator_returnsUpdatedDomainSeparatorIfChainIDIsDifferentFromValueSetOnDeployment() public { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); + + bytes32 domainSeparator = zone.exposed_deriveDomainSeparator(); + assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); + } + /* _getSupportedSubstandards */ function test_getSupportedSubstandards() public { @@ -907,7 +977,7 @@ contract ImmutableSignedZoneV2Test is zone.exposed_validateSubstandard4(context, zoneParameters); } - function test_validateSubstandard4_revertsIfDerivedOrderHashesIsNotEqualToHashesInContext() public { + function test_validateSubstandard4_revertsIfExpectedOrderHashesAreNotPresent() public { ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); bytes32[] memory orderHashes = new bytes32[](1); @@ -1135,23 +1205,13 @@ contract ImmutableSignedZoneV2Test is recipient: payable(address(0x3)) }); - // console.logBytes32(zone.exposed_deriveReceivedItemsHash(receivedItems, MAX_UINT_TYPE, 100)); - bytes32 receivedItemsHash = zone.exposed_deriveReceivedItemsHash(receivedItems, MAX_UINT_TYPE, 100); + // 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_returnsFalseIfSourceArrayIsEmpty() public { - ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); - - bytes32[] memory emptySourceArray = new bytes32[](0); - bytes32[] memory valuesArray = new bytes32[](2); - - bool includes = zone.exposed_bytes32ArrayIncludes(emptySourceArray, valuesArray); - assertFalse(includes); - } - function test_bytes32ArrayIncludes_returnsFalseIfSourceArrayIsSmallerThanValuesArray() public { ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); @@ -1176,7 +1236,7 @@ contract ImmutableSignedZoneV2Test is assertFalse(includes); } - function test_bytes32ArrayIncludes_returnsTrueIfSourceArrayIncludesValuesArray() public { + function test_bytes32ArrayIncludes_returnsTrueIfSourceArrayEqualsValuesArray() public { ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); bytes32[] memory sourceArray = new bytes32[](2); @@ -1206,38 +1266,9 @@ contract ImmutableSignedZoneV2Test is assertTrue(includes); } - /* _domainSeparator */ - - function test_domainSeparator_returnsCachedDomainSeparatorWhenChainIDMatchesValueSetOnDeployment() public { - ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); - - bytes32 domainSeparator = zone.exposed_domainSeparator(); - assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); - } - - function test_domainSeparator_returnsUpdatedDomainSeparatorIfChainIDIsDifferentFromValueSetOnDeployment() public { - ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); - - bytes32 domainSeparatorCached = zone.exposed_domainSeparator(); - vm.chainId(31338); - bytes32 domainSeparatorDerived = zone.exposed_domainSeparator(); - - assertFalse(domainSeparatorCached == domainSeparatorDerived); - assertEq(domainSeparatorDerived, bytes32(0x835aabb0d2af048df195a75a990b42533471d4a4e82842cd54a892eaac463d74)); - } - - /* _deriveDomainSeparator */ - - function test_deriveDomainSeparator_returnsDomainSeparatorForChainID() public { - ImmutableSignedZoneV2Harness zone = _newZoneHarness(OWNER); - - bytes32 domainSeparator = zone.exposed_deriveDomainSeparator(); - assertEq(domainSeparator, bytes32(0xafb48e1c246f21ba06352cb2c0ebe99b8adc2590dfc48fa547732df870835b42)); - } - /* helper functions */ - function _newZone(address owner) internal returns (ImmutableSignedZoneV2) { + function _newZone(address owner) private returns (ImmutableSignedZoneV2) { return new ImmutableSignedZoneV2( "MyZoneName", "https://www.immutable.com", @@ -1246,7 +1277,7 @@ contract ImmutableSignedZoneV2Test is ); } - function _newZoneHarness(address owner) internal returns (ImmutableSignedZoneV2Harness) { + function _newZoneHarness(address owner) private returns (ImmutableSignedZoneV2Harness) { return new ImmutableSignedZoneV2Harness( "MyZoneName", "https://www.immutable.com", @@ -1262,7 +1293,7 @@ contract ImmutableSignedZoneV2Test is uint64 expiration, bytes32 orderHash, bytes memory context - ) internal view returns (bytes memory) { + ) private view returns (bytes memory) { bytes32 eip712SignedOrderHash = zone.exposed_deriveSignedOrderHash(fulfiller, expiration, orderHash, context); bytes memory extraData = abi.encodePacked( bytes1(0), diff --git a/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol index 9aeaea26..264fb57e 100644 --- a/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol @@ -15,6 +15,14 @@ contract ImmutableSignedZoneV2Harness is ImmutableSignedZoneV2 { ImmutableSignedZoneV2(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(); } @@ -24,7 +32,7 @@ contract ImmutableSignedZoneV2Harness is ImmutableSignedZoneV2 { uint64 expiration, bytes32 orderHash, bytes calldata context - ) external view returns (bytes32 signedOrderHash) { + ) external pure returns (bytes32 signedOrderHash) { return _deriveSignedOrderHash(fulfiller, expiration, orderHash, context); } @@ -74,14 +82,6 @@ contract ImmutableSignedZoneV2Harness is ImmutableSignedZoneV2 { { return _bytes32ArrayIncludes(sourceArray, values); } - - function exposed_domainSeparator() external view returns (bytes32) { - return _domainSeparator(); - } - - function exposed_deriveDomainSeparator() external view returns (bytes32 domainSeparator) { - return _deriveDomainSeparator(); - } } // solhint-enable func-name-mixedcase diff --git a/test/trading/seaport/zones/immutable-signed-zone/v2/README.md b/test/trading/seaport/zones/immutable-signed-zone/v2/README.md new file mode 100644 index 00000000..dee26fac --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/README.md @@ -0,0 +1,89 @@ +# 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_addSigner_revertsIfCalledByNonAdminRole | 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_revertsIfCalledByNonAdminRole | 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_revertsIfCalledByNonAdminRole | Update API endpoint without authorization. | No | Yes | +| test_updateAPIEndpoint_updatesAPIEndpointIfCalledByAdminRole | Update API endpoint 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_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_emptyContext | Empty context without substandards. | 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_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 | \ No newline at end of file