diff --git a/.gitmodules b/.gitmodules index 24318171..dacdffa1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "lib/immutable-seaport-core-1.5.0+im1"] path = lib/immutable-seaport-core-1.5.0+im1 url = https://github.com/immutable/seaport-core +[submodule "lib/openzeppelin-contracts-5.0.2"] + path = lib/openzeppelin-contracts-5.0.2 + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/trading/seaport/zones/ImmutableSignedZone.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol similarity index 99% rename from contracts/trading/seaport/zones/ImmutableSignedZone.sol rename to contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol index 52f71a3a..08d1ed17 100644 --- a/contracts/trading/seaport/zones/ImmutableSignedZone.sol +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone.sol @@ -1,7 +1,8 @@ // Copyright (c) Immutable Pty Ltd 2018 - 2023 // SPDX-License-Identifier: Apache-2 -// solhint-disable compiler-version + // slither-disable-start missing-inheritance +// solhint-disable-next-line compiler-version pragma solidity ^0.8.17; import {ZoneParameters, Schema, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; diff --git a/contracts/trading/seaport/zones/README.md b/contracts/trading/seaport/zones/immutable-signed-zone/v1/README.md similarity index 100% rename from contracts/trading/seaport/zones/README.md rename to contracts/trading/seaport/zones/immutable-signed-zone/v1/README.md diff --git a/contracts/trading/seaport/zones/interfaces/SIP5Interface.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP5Interface.sol similarity index 100% rename from contracts/trading/seaport/zones/interfaces/SIP5Interface.sol rename to contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP5Interface.sol diff --git a/contracts/trading/seaport/zones/interfaces/SIP6EventsAndErrors.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP6EventsAndErrors.sol similarity index 100% rename from contracts/trading/seaport/zones/interfaces/SIP6EventsAndErrors.sol rename to contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP6EventsAndErrors.sol diff --git a/contracts/trading/seaport/zones/interfaces/SIP7EventsAndErrors.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol similarity index 100% rename from contracts/trading/seaport/zones/interfaces/SIP7EventsAndErrors.sol rename to contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7EventsAndErrors.sol diff --git a/contracts/trading/seaport/zones/interfaces/SIP7Interface.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7Interface.sol similarity index 100% rename from contracts/trading/seaport/zones/interfaces/SIP7Interface.sol rename to contracts/trading/seaport/zones/immutable-signed-zone/v1/interfaces/SIP7Interface.sol diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol new file mode 100644 index 00000000..b5d92925 --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol @@ -0,0 +1,594 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ZoneParameters, Schema, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ZoneInterface} from "seaport/contracts/interfaces/ZoneInterface.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 + * @author Immutable + * @notice ImmutableSignedZoneV2 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. + */ +contract ImmutableSignedZoneV2 is + ERC165, + ZoneInterface, + SIP5Interface, + SIP6Interface, + 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( + abi.encodePacked( + "EIP712Domain(", "string name,", "string version,", "uint256 chainId,", "address verifyingContract", ")" + ) + ); + + bytes32 internal immutable _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 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 + + /// @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; + + /// @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) { + // Set the zone name. + _ZONE_NAME = zoneName; + + // Set name hash. + _NAME_HASH = keccak256(bytes(zoneName)); + + // Set the API endpoint. + _sip7APIEndpoint = apiEndpoint; + _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(); + + // Grant admin role to the specified owner. + _grantRole(DEFAULT_ADMIN_ROLE, owner); + } + + /** + * @notice Add a new signer to the zone. + * + * @param signer The new signer address to add. + */ + function addSigner(address signer) external override onlyRole(DEFAULT_ADMIN_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(DEFAULT_ADMIN_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(DEFAULT_ADMIN_ROLE) { + // Update to the new API endpoint. + _sip7APIEndpoint = newApiEndpoint; + } + + /** + * @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(), _sip7APIEndpoint, _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 = _sip7APIEndpoint; + + 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. + * + * @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 = ECDSA.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; + } + + /** + * @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 + view + 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; + + // Each _validateSubstandard* function returns the length of the substandard + // segment (0 if the substandard was not matched). + + if (startIndex == contextLength) return; + 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; + + 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; + } + + /** + * @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/interfaces/SIP5EventsAndErrors.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP5EventsAndErrors.sol new file mode 100644 index 00000000..21bbf159 --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/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/seaport/zones/immutable-signed-zone/v2/interfaces/SIP5Interface.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP5Interface.sol new file mode 100644 index 00000000..cfc156fd --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP5Interface.sol @@ -0,0 +1,23 @@ +// 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/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 + */ +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/seaport/zones/immutable-signed-zone/v2/interfaces/SIP6EventsAndErrors.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP6EventsAndErrors.sol new file mode 100644 index 00000000..b4244c30 --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP6EventsAndErrors.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 SIP6EventsAndErrors contains errors and events + * related to zone interaction as specified in the SIP-6. + */ +interface SIP6EventsAndErrors { + /** + * @dev Revert with an error if SIP-6 version byte is not supported. + */ + error UnsupportedExtraDataVersion(uint8 version); +} diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP6Interface.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP6Interface.sol new file mode 100644 index 00000000..2bc63899 --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/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 {Schema} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {SIP6EventsAndErrors} from "./SIP6EventsAndErrors.sol"; + +/** + * @dev SIP-6: Multi-Zone ExtraData + * https://github.com/ProjectOpenSea/SIPs/blob/main/SIPS/sip-6.md + */ +interface SIP6Interface is SIP6EventsAndErrors {} diff --git a/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7EventsAndErrors.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7EventsAndErrors.sol new file mode 100644 index 00000000..52c0877b --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7EventsAndErrors.sol @@ -0,0 +1,84 @@ +// 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. + */ +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/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol new file mode 100644 index 00000000..c09fad68 --- /dev/null +++ b/contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol @@ -0,0 +1,65 @@ +// 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 + * + */ +interface SIP7Interface is SIP7EventsAndErrors { + /** + * @dev The struct for storing signer info. + */ + struct SignerInfo { + /// If the signer is currently active. + bool active; + /// 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 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/foundry.toml b/foundry.toml index 9c682eb7..8e6b1d92 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,7 @@ src = 'contracts' out = 'foundry-out' # 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 e513296e..2dc610a3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -35,6 +35,15 @@ const config: HardhatUserConfig = { }, }, }, + { + version: "0.8.20", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, { version: "0.8.17", settings: { diff --git a/lib/openzeppelin-contracts-5.0.2 b/lib/openzeppelin-contracts-5.0.2 new file mode 160000 index 00000000..dbb6104c --- /dev/null +++ b/lib/openzeppelin-contracts-5.0.2 @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/package.json b/package.json index c47bebdb..e43424e8 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@openzeppelin/contracts-upgradeable": "^4.9.3", "@rari-capital/solmate": "^6.4.0", "eslint-plugin-mocha": "^10.2.0", + "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", "solidity-bits": "^0.4.0", diff --git a/remappings.txt b/remappings.txt index a84ebf6c..3323b1c8 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ @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/ solidity-bits/=lib/solidity-bits/ solidity-bytes-utils/=lib/solidity-bytes-utils/ diff --git a/test/trading/seaport/ImmutableSeaportHarness.t.sol b/test/trading/seaport/ImmutableSeaportHarness.t.sol new file mode 100644 index 00000000..15de0c88 --- /dev/null +++ b/test/trading/seaport/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/seaport/ImmutableSeaport.sol"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSeaportHarness is ImmutableSeaport { + constructor(address conduitController, address owner) ImmutableSeaport(conduitController, owner) {} + + function exposed_deriveEIP712Digest(bytes32 domainSeparator, bytes32 orderHash) + external + pure + returns (bytes32 value) + { + 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 new file mode 100644 index 00000000..6494fc8b --- /dev/null +++ b/test/trading/seaport/ImmutableSeaportSignedZoneV2Integration.t.sol @@ -0,0 +1,896 @@ +// 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 {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 { + AdvancedOrder, + ConsiderationItem, + CriteriaResolver, + OrderComponents, + OfferItem, + 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"; + +// 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 = + "./foundry-out/OperatorAllowlistUpgradeable.sol/OperatorAllowlistUpgradeable.json"; + string internal constant ERC1155_ARTIFACT = "./foundry-out/ImmutableERC1155.sol/ImmutableERC1155.json"; + string internal 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 = + "./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; + + 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 = IImmutableSignedZoneV2Harness( + deployCode( + ZONE_ARTIFACT, + abi.encode("MyZoneName", "https://www.immutable.com", "https://www.immutable.com/docs", OWNER) + ) + ); + vm.prank(OWNER); + 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/seaport/utils/IImmutableERC1155.t.sol b/test/trading/seaport/utils/IImmutableERC1155.t.sol new file mode 100644 index 00000000..c32b7946 --- /dev/null +++ b/test/trading/seaport/utils/IImmutableERC1155.t.sol @@ -0,0 +1,27 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +/** + * @notice Interface for Immutable's ERC1155 + */ +interface IImmutableERC1155 is IERC1155 { + /** + * @notice Mints a new token + * @param to The address that will receive the minted tokens + * @param id The id of the token to mint + * @param value The amount of tokens to mint + * @param data Additional data + */ + function safeMint(address to, uint256 id, uint256 value, bytes memory data) external; + + /** + * @notice Grants minter role to the user + * @param user The address to grant the MINTER_ROLE to + */ + function grantMinterRole(address user) external; +} diff --git a/test/trading/seaport/utils/IImmutableERC721.t.sol b/test/trading/seaport/utils/IImmutableERC721.t.sol new file mode 100644 index 00000000..c03f9a12 --- /dev/null +++ b/test/trading/seaport/utils/IImmutableERC721.t.sol @@ -0,0 +1,23 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @notice Interface for Immutable's ERC721 + */ +interface IImmutableERC721 is IERC721 { + /** @notice Allows minter to mint `tokenID` to `to` + * @param to the address to mint the token to + * @param tokenID the ID of the token to mint + */ + function safeMint(address to, uint256 tokenID) external; + + /** @notice Allows admin grant `user` `MINTER` role + * @param user The address to grant the `MINTER` role to + */ + function grantMinterRole(address user) external; +} diff --git a/test/trading/seaport/utils/IOperatorAllowlistUpgradeable.t.sol b/test/trading/seaport/utils/IOperatorAllowlistUpgradeable.t.sol new file mode 100644 index 00000000..f521d61b --- /dev/null +++ b/test/trading/seaport/utils/IOperatorAllowlistUpgradeable.t.sol @@ -0,0 +1,23 @@ +// Copyright Immutable Pty Ltd 2018 - 2023 +// SPDX-License-Identifier: Apache 2.0 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +/** + * @notice Required interface of an OperatorAllowlist compliant contract + */ +interface IOperatorAllowlistUpgradeable { + /** + * @notice Grants `DEFAULT_ADMIN_ROLE` to the supplied `admin` address + * @param _roleAdmin the address to grant `DEFAULT_ADMIN_ROLE` to + * @param _upgradeAdmin the address to grant `UPGRADE_ROLE` to + */ + function initialize(address _roleAdmin, address _upgradeAdmin, address _registerarAdmin) external; + + /** + * @notice Adds a list of multiple addresses to Allowlist + * @param addressTargets the addresses to be added to the allowlist + */ + function addAddressesToAllowlist(address[] calldata addressTargets) external; +} diff --git a/test/trading/seaport/utils/SigningTestHelper.t.sol b/test/trading/seaport/utils/SigningTestHelper.t.sol new file mode 100644 index 00000000..33f56497 --- /dev/null +++ b/test/trading/seaport/utils/SigningTestHelper.t.sol @@ -0,0 +1,24 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +// solhint-disable-next-line no-global-import +import "forge-std/Test.sol"; + +abstract contract SigningTestHelper is Test { + function _sign(uint256 signerPrivateKey, bytes32 signatureDigest) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, signatureDigest); + return abi.encodePacked(r, s, v); + } + + function _signCompact(uint256 signerPrivateKey, bytes32 signatureDigest) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, signatureDigest); + if (v != 27) { + // then left-most bit of s has to be flipped to 1. + s = s | bytes32(uint256(1) << 255); + } + return abi.encodePacked(r, s); + } +} diff --git a/test/trading/seaport/zones/immutablesignedzone.test.ts b/test/trading/seaport/zones/immutable-signed-zone/v1/immutablesignedzone.test.ts similarity index 97% rename from test/trading/seaport/zones/immutablesignedzone.test.ts rename to test/trading/seaport/zones/immutable-signed-zone/v1/immutablesignedzone.test.ts index 1c9b024d..808e0b2a 100644 --- a/test/trading/seaport/zones/immutablesignedzone.test.ts +++ b/test/trading/seaport/zones/immutable-signed-zone/v1/immutablesignedzone.test.ts @@ -5,7 +5,7 @@ import { Wallet, constants } from "ethers"; import { keccak256 } from "ethers/lib/utils"; import { ethers } from "hardhat"; -import { ImmutableSignedZone__factory } from "../../../../typechain-types"; +import { ImmutableSignedZone__factory } from "../../../../../../typechain-types"; import { CONSIDERATION_EIP712_TYPE, @@ -15,13 +15,13 @@ import { autoMining, convertSignatureToEIP2098, getCurrentTimeStamp, -} from "../utils/signedZone"; +} from "../../../utils/signedZone"; -import type { ImmutableSignedZone } from "../../../../typechain-types"; +import type { ImmutableSignedZone } from "../../../../../../typechain-types"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import type { BytesLike } from "ethers"; -import { ReceivedItemStruct } from "../../../../typechain-types/contracts/trading/seaport/ImmutableSeaport"; -import { ZoneParametersStruct } from "../../../../typechain-types/contracts/trading/seaport/zones/ImmutableSignedZone"; +import { ReceivedItemStruct } from "../../../../../../typechain-types/contracts/trading/seaport/ImmutableSeaport"; +import { ZoneParametersStruct } from "../../../../../../typechain-types/contracts/trading/seaport/zones/immutable-signed-zone/v1/ImmutableSignedZone"; describe("ImmutableSignedZone", function () { let deployer: SignerWithAddress; 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 new file mode 100644 index 00000000..5b4dcd03 --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/IImmutableSignedZoneV2Harness.t.sol @@ -0,0 +1,59 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ZoneInterface} from "seaport/contracts/interfaces/ZoneInterface.sol"; +import {ReceivedItem, ZoneParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {SIP7Interface} from + "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v2/interfaces/SIP7Interface.sol"; + +// solhint-disable func-name-mixedcase + +interface IImmutableSignedZoneV2Harness is ZoneInterface, SIP7Interface { + 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 + 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); + + 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 new file mode 100644 index 00000000..a822d45d --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.t.sol @@ -0,0 +1,1278 @@ +// 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 {ReceivedItem, Schema, SpentItem, ZoneParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.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"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSignedZoneV2Test is + Test, + SigningTestHelper, + SIP5EventsAndErrors, + 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; + // solhint-enable private-vars-leading-underscore + + constructor() { + (SIGNER, SIGNER_PRIVATE_KEY) = makeAddrAndKey("signer"); + } + + /* constructor */ + + function test_contructor_grantsAdminRoleToOwner() public { + address owner = makeAddr("owner"); + ImmutableSignedZoneV2 zone = new ImmutableSignedZoneV2( + "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 ImmutableSignedZoneV2( + "MyZoneName", + "https://www.immutable.com", + "https://www.immutable.com/docs", + makeAddr("owner") + ); + } + + /* addSigner */ + + function test_addSigner_revertsIfCalledByNonAdminRole() public { + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.expectRevert( + "AccessControl: account 0x42a3d6e125aad539ac15ed04e1478eb0a4dc1489 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" + ); + vm.prank(makeAddr("random")); + zone.addSigner(makeAddr("signerToAdd")); + } + + function test_addSigner_revertsIfSignerIsTheZeroAddress() public { + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.expectRevert(abi.encodeWithSelector(SignerCannotBeZeroAddress.selector)); + vm.prank(OWNER); + zone.addSigner(address(0)); + } + + function test_addSigner_emitsSignerAddedEvent() public { + address signerToAdd = makeAddr("signerToAdd"); + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.expectEmit(address(zone)); + emit SignerAdded(signerToAdd); + vm.prank(OWNER); + zone.addSigner(signerToAdd); + } + + function test_addSigner_revertsIfSignerAlreadyActive() public { + address signerToAdd = makeAddr("signerToAdd"); + ImmutableSignedZoneV2 zone = _newZone(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("signerToAdd"); + ImmutableSignedZoneV2 zone = _newZone(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_revertsIfCalledByNonAdminRole() public { + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.expectRevert( + "AccessControl: account 0x42a3d6e125aad539ac15ed04e1478eb0a4dc1489 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" + ); + vm.prank(makeAddr("random")); + zone.removeSigner(makeAddr("signerToRemove")); + } + + function test_removeSigner_revertsIfSignerNotActive() public { + address signerToRemove = makeAddr("signerToRemove"); + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.expectRevert(abi.encodeWithSelector(SignerNotActive.selector, signerToRemove)); + vm.prank(OWNER); + zone.removeSigner(signerToRemove); + } + + function test_removeSigner_emitsSignerRemovedEvent() public { + address signerToRemove = makeAddr("signerToRemove"); + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.prank(OWNER); + zone.addSigner(signerToRemove); + vm.expectEmit(address(zone)); + emit SignerRemoved(signerToRemove); + vm.prank(OWNER); + zone.removeSigner(signerToRemove); + } + + /* updateAPIEndpoint */ + + function test_updateAPIEndpoint_revertsIfCalledByNonAdminRole() public { + ImmutableSignedZoneV2 zone = _newZone(OWNER); + vm.prank(makeAddr("random")); + vm.expectRevert( + "AccessControl: account 0x42a3d6e125aad539ac15ed04e1478eb0a4dc1489 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" + ); + 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"; + zone.updateAPIEndpoint(expectedApiEndpoint); + (, Schema[] memory schemas) = zone.getSeaportMetadata(); + (, string memory apiEndpoint,,) = abi.decode(schemas[0].metadata, (bytes32, string, uint256[], string)); + assertEq(apiEndpoint, expectedApiEndpoint); + } + + /* getSeaportMetadata */ + + function test_getSeaportMetadata() public { + string memory expectedZoneName = "MyZoneName"; + string memory expectedApiEndpoint = "https://www.immutable.com"; + string memory expectedDocumentationURI = "https://www.immutable.com/docs"; + + ImmutableSignedZoneV2Harness zone = new ImmutableSignedZoneV2Harness( + 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"; + + ImmutableSignedZoneV2Harness zone = new ImmutableSignedZoneV2Harness( + "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); + } + + /* 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 { + ImmutableSignedZoneV2 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.validateOrder(zoneParameters); + } + + function test_validateOrder_revertsIfExtraDataLengthIsLessThan93() public { + ImmutableSignedZoneV2 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.validateOrder(zoneParameters); + } + + function test_validateOrder_revertsIfExtraDataVersionIsNotSupported() public { + ImmutableSignedZoneV2 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.validateOrder(zoneParameters); + } + + function test_validateOrder_revertsIfSignatureHasExpired() public { + ImmutableSignedZoneV2Harness 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.validateOrder(zoneParameters); + } + + function test_validateOrder_revertsIfActualFulfillerDoesNotMatchExpectedFulfiller() public { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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(SignerNotActive.selector, address(0x6E12D8C87503D4287c294f2Fdef96ACd9DFf6bd2)) + ); + zone.validateOrder(zoneParameters); + } + + function test_validateOrder_returnsMagicValueOnSuccessfulValidation() public { + ImmutableSignedZoneV2Harness zone = _newZoneHarness(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.validateOrder(zoneParameters), bytes4(0x17b1f942)); + } + + /* _getSupportedSubstandards */ + + function test_getSupportedSubstandards() public { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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_emptyContext() public { + ImmutableSignedZoneV2Harness 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) + }); + + zone.exposed_validateSubstandards(new bytes(0), zoneParameters); + } + + function test_validateSubstandards_substandard3() public { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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_revertsIfDerivedOrderHashesIsNotEqualToHashesInContext() public { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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, MAX_UINT_TYPE, 100)); + bytes32 receivedItemsHash = zone.exposed_deriveReceivedItemsHash(receivedItems, MAX_UINT_TYPE, 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); + + 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 { + ImmutableSignedZoneV2Harness 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_returnsTrueIfSourceArrayIncludesValuesArray() public { + ImmutableSignedZoneV2Harness 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 { + ImmutableSignedZoneV2Harness 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); + } + + /* _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) { + return new ImmutableSignedZoneV2( + "MyZoneName", + "https://www.immutable.com", + "https://www.immutable.com/docs", + owner + ); + } + + function _newZoneHarness(address owner) internal returns (ImmutableSignedZoneV2Harness) { + return new ImmutableSignedZoneV2Harness( + "MyZoneName", + "https://www.immutable.com", + "https://www.immutable.com/docs", + owner + ); + } + + function _buildExtraData( + ImmutableSignedZoneV2Harness zone, + uint256 signerPrivateKey, + address fulfiller, + uint64 expiration, + bytes32 orderHash, + bytes memory context + ) internal view returns (bytes memory) { + bytes32 eip712SignedOrderHash = zone.exposed_deriveSignedOrderHash(fulfiller, expiration, orderHash, context); + bytes memory extraData = abi.encodePacked( + bytes1(0), + fulfiller, + expiration, + _signCompact(signerPrivateKey, ECDSA.toTypedDataHash(zone.exposed_domainSeparator(), eip712SignedOrderHash)), + context + ); + return extraData; + } +} + +// solhint-enable func-name-mixedcase diff --git a/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol new file mode 100644 index 00000000..9aeaea26 --- /dev/null +++ b/test/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2Harness.t.sol @@ -0,0 +1,87 @@ +// Copyright (c) Immutable Pty Ltd 2018 - 2024 +// SPDX-License-Identifier: Apache-2 + +// solhint-disable-next-line compiler-version +pragma solidity ^0.8.17; + +import {ReceivedItem, ZoneParameters} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ImmutableSignedZoneV2} from + "../../../../../../contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol"; + +// solhint-disable func-name-mixedcase + +contract ImmutableSignedZoneV2Harness is ImmutableSignedZoneV2 { + constructor(string memory zoneName, string memory apiEndpoint, string memory documentationURI, address owner) + ImmutableSignedZoneV2(zoneName, apiEndpoint, documentationURI, owner) + {} + + function exposed_getSupportedSubstandards() external pure returns (uint256[] memory substandards) { + return _getSupportedSubstandards(); + } + + function exposed_deriveSignedOrderHash( + address fulfiller, + uint64 expiration, + bytes32 orderHash, + bytes calldata context + ) external view returns (bytes32 signedOrderHash) { + return _deriveSignedOrderHash(fulfiller, expiration, orderHash, context); + } + + function exposed_validateSubstandards(bytes calldata context, ZoneParameters calldata zoneParameters) + external + pure + { + return _validateSubstandards(context, zoneParameters); + } + + function exposed_validateSubstandard3(bytes calldata context, ZoneParameters calldata zoneParameters) + external + pure + returns (uint256) + { + return _validateSubstandard3(context, zoneParameters); + } + + function exposed_validateSubstandard4(bytes calldata context, ZoneParameters calldata zoneParameters) + external + 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); + } + + 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/yarn.lock b/yarn.lock index 632dc7eb..c16f2a84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1071,7 +1071,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== @@ -6335,6 +6335,16 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +"openzeppelin-contracts-5.0.2@npm:@openzeppelin/contracts@^5.0.2": + version "5.0.2" + 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" @@ -7639,7 +7649,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== @@ -7665,6 +7675,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" @@ -7715,7 +7734,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== @@ -7736,6 +7755,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" @@ -8918,7 +8944,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== @@ -8935,6 +8961,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"