diff --git a/foundry.toml b/foundry.toml index 06c48b7..1bed5bf 100644 --- a/foundry.toml +++ b/foundry.toml @@ -45,7 +45,7 @@ src = 'reference' out = 'reference-out' script = 'reference' # specify something so it doesn't try to compile the files in test/foundry -test = 'reference' +test = 'test/foundry' [profile.optimized] src = 'src' diff --git a/test/foundry/new/FuzzInscribers.t.sol b/test/foundry/new/FuzzInscribers.t.sol index 00ce2ee..b65cf82 100644 --- a/test/foundry/new/FuzzInscribers.t.sol +++ b/test/foundry/new/FuzzInscribers.t.sol @@ -339,6 +339,8 @@ contract FuzzHelpersTest is BaseOrderTest { == keccak256(abi.encodePacked("test")) || keccak256(abi.encodePacked(profile)) == keccak256(abi.encodePacked("lite")) + || keccak256(abi.encodePacked(profile)) + == keccak256(abi.encodePacked("reference")) ) { expectedReadAccessCount = 1; } diff --git a/test/foundry/new/helpers/FuzzInscribers.sol b/test/foundry/new/helpers/FuzzInscribers.sol index 277f1a8..dd5bd27 100644 --- a/test/foundry/new/helpers/FuzzInscribers.sol +++ b/test/foundry/new/helpers/FuzzInscribers.sol @@ -297,6 +297,8 @@ library FuzzInscribers { == keccak256(abi.encodePacked("test")) || keccak256(abi.encodePacked(profile)) == keccak256(abi.encodePacked("lite")) + || keccak256(abi.encodePacked(profile)) + == keccak256(abi.encodePacked("reference")) ) { expectedReadAccessCount = 1; } diff --git a/test/foundry/zone/PostFulfillmentCheck.t.sol b/test/foundry/zone/PreAndPostFulfillmentCheck.t.sol similarity index 91% rename from test/foundry/zone/PostFulfillmentCheck.t.sol rename to test/foundry/zone/PreAndPostFulfillmentCheck.t.sol index 7541bc5..910260c 100644 --- a/test/foundry/zone/PostFulfillmentCheck.t.sol +++ b/test/foundry/zone/PreAndPostFulfillmentCheck.t.sol @@ -3,13 +3,11 @@ pragma solidity ^0.8.17; import { BaseOrderTest } from "../utils/BaseOrderTest.sol"; -import { TestZone } from "./impl/TestZone.sol"; - import { TestTransferValidationZoneOfferer } from "seaport/test/TestTransferValidationZoneOfferer.sol"; -import { PostFulfillmentStatefulTestZone } from - "./impl/PostFullfillmentStatefulTestZone.sol"; +import { StatefulTestZone } from + "./impl/StatefulTestZone.sol"; import { AdditionalRecipient, @@ -31,10 +29,9 @@ import { import { ConsiderationInterface } from "seaport-types/src/interfaces/ConsiderationInterface.sol"; -contract PostFulfillmentCheckTest is BaseOrderTest { - TestZone zone = new TestZone(); - PostFulfillmentStatefulTestZone statefulZone = - new PostFulfillmentStatefulTestZone(50); +contract PreAndPostFulfillmentCheckTest is BaseOrderTest { + StatefulTestZone statefulZone = + new StatefulTestZone(50); struct Context { ConsiderationInterface consideration; @@ -42,11 +39,6 @@ contract PostFulfillmentCheckTest is BaseOrderTest { uint8 numTips; } - struct EthConsideration { - address payable recipient; - uint256 amount; - } - function test(function(Context memory) external fn, Context memory context) internal { @@ -63,18 +55,18 @@ contract PostFulfillmentCheckTest is BaseOrderTest { referenceConduitController.updateChannel( address(referenceConduit), address(this), true ); - vm.label(address(zone), "TestZone"); + vm.label(address(statefulZone), "TestZone"); } function testAscendingAmount() public { - test( - this.execAscendingAmount, - Context({ - consideration: consideration, - numOriginalAdditional: 0, - numTips: 0 - }) - ); + // test( + // this.execAscendingAmount, + // Context({ + // consideration: consideration, + // numOriginalAdditional: 0, + // numTips: 0 + // }) + // ); test( this.execAscendingAmount, Context({ @@ -126,14 +118,14 @@ contract PostFulfillmentCheckTest is BaseOrderTest { } function testResolvedCriteria() public { - test( - this.execResolvedCriteria, - Context({ - consideration: consideration, - numOriginalAdditional: 0, - numTips: 0 - }) - ); + // test( + // this.execResolvedCriteria, + // Context({ + // consideration: consideration, + // numOriginalAdditional: 0, + // numTips: 0 + // }) + // ); test( this.execResolvedCriteria, Context({ @@ -193,14 +185,14 @@ contract PostFulfillmentCheckTest is BaseOrderTest { } function testStateChange() public { - test( - this.execStateChange, - Context({ - consideration: consideration, - numOriginalAdditional: 0, - numTips: 0 - }) - ); + // test( + // this.execStateChange, + // Context({ + // consideration: consideration, + // numOriginalAdditional: 0, + // numTips: 0 + // }) + // ); test( this.execStateChange, Context({ @@ -258,18 +250,19 @@ contract PostFulfillmentCheckTest is BaseOrderTest { recipient: address(0) }); - assertTrue(statefulZone.called()); + assertTrue(statefulZone.authorizeCalled()); + assertTrue(statefulZone.validateCalled()); } function testBasicStateful() public { - test( - this.execBasicStateful, - Context({ - consideration: consideration, - numOriginalAdditional: 0, - numTips: 0 - }) - ); + // test( + // this.execBasicStateful, + // Context({ + // consideration: consideration, + // numOriginalAdditional: 0, + // numTips: 0 + // }) + // ); test( this.execBasicStateful, Context({ @@ -326,14 +319,14 @@ contract PostFulfillmentCheckTest is BaseOrderTest { } function testExectBasicStatefulWithConduit() public { - test( - this.execBasicStatefulWithConduit, - Context({ - consideration: consideration, - numOriginalAdditional: 0, - numTips: 0 - }) - ); + // test( + // this.execBasicStatefulWithConduit, + // Context({ + // consideration: consideration, + // numOriginalAdditional: 0, + // numTips: 0 + // }) + // ); test( this.execBasicStatefulWithConduit, Context({ @@ -395,14 +388,14 @@ contract PostFulfillmentCheckTest is BaseOrderTest { function testBasicStateful(uint8 numOriginalAdditional, uint8 numTips) public { - test( - this.execBasicStatefulFuzz, - Context({ - consideration: consideration, - numOriginalAdditional: numOriginalAdditional, - numTips: numTips - }) - ); + // test( + // this.execBasicStatefulFuzz, + // Context({ + // consideration: consideration, + // numOriginalAdditional: numOriginalAdditional, + // numTips: numTips + // }) + // ); test( this.execBasicStatefulFuzz, Context({ @@ -420,7 +413,7 @@ contract PostFulfillmentCheckTest is BaseOrderTest { ); // make new stateful zone with a larger amount so each additional // recipient can receive - statefulZone = new PostFulfillmentStatefulTestZone(5000); + statefulZone = new StatefulTestZone(5000); // clear storage array just in case delete additionalRecipients; @@ -498,7 +491,8 @@ contract PostFulfillmentCheckTest is BaseOrderTest { }); // assertions - assertTrue(statefulZone.called()); + assertTrue(statefulZone.authorizeCalled()); + assertTrue(statefulZone.validateCalled()); for (uint256 i = 0; i < allAdditional.length; i++) { assertEq( token1.balanceOf(allAdditional[i]), @@ -509,14 +503,14 @@ contract PostFulfillmentCheckTest is BaseOrderTest { } function testFulfillAvailableAdvancedAscending() public { - test( - this.execFulfillAvailableAdvancedAscending, - Context({ - consideration: consideration, - numOriginalAdditional: 0, - numTips: 0 - }) - ); + // test( + // this.execFulfillAvailableAdvancedAscending, + // Context({ + // consideration: consideration, + // numOriginalAdditional: 0, + // numTips: 0 + // }) + // ); test( this.execFulfillAvailableAdvancedAscending, Context({ @@ -584,7 +578,8 @@ contract PostFulfillmentCheckTest is BaseOrderTest { recipient: address(0), maximumFulfilled: 1 }); - assertTrue(statefulZone.called()); + assertTrue(statefulZone.authorizeCalled()); + assertTrue(statefulZone.validateCalled()); } function testExecMatchAdvancedOrdersWithConduit() public { diff --git a/test/foundry/zone/TestZoneCalldataFidelity.t.sol b/test/foundry/zone/TestZoneCalldataFidelity.t.sol new file mode 100644 index 0000000..77c9843 --- /dev/null +++ b/test/foundry/zone/TestZoneCalldataFidelity.t.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { + ConsiderationItemLib, + OfferItemLib, + OrderParametersLib +} from "seaport-sol/src/SeaportSol.sol"; + +import { BaseOrderTest } from "../utils/BaseOrderTest.sol"; + +import { HashCalldataTestZone } from "./impl/HashCalldataTestZone.sol"; + +import { + AdvancedOrder, + ConsiderationItem, + CriteriaResolver, + ItemType, + OfferItem, + OrderParameters, + ZoneParameters +} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import { OrderType } from "seaport-types/src/lib/ConsiderationEnums.sol"; + +import { ConsiderationInterface } from + "seaport-types/src/interfaces/ConsiderationInterface.sol"; + + +contract PreAndPostFulfillmentCheckTest is BaseOrderTest { + using ConsiderationItemLib for ConsiderationItem[]; + using OfferItemLib for OfferItem[]; + using OrderParametersLib for OrderParameters; + + HashCalldataTestZone testZone = + new HashCalldataTestZone(); + + struct Context { + ConsiderationInterface consideration; + TestCase testCase; + } + + struct TestCase { + uint256 itemType; + uint256 offerItemStartingIdentifier; + uint256 considerationItemStartingIdentifier; + uint256 startAmount; + uint256 endAmount; + bytes signature; + bytes extraData; + uint256 recipient; + uint256 offerLength; + uint256 considerationLength; + uint256 orderType; + uint256 startTime; + uint256 endTime; + uint256 zoneHash; + uint256 salt; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) { + fail(); + } catch (bytes memory reason) { + assertPass(reason); + } + } + + function setUp() public override { + super.setUp(); + conduitController.updateChannel(address(conduit), address(this), true); + referenceConduitController.updateChannel( + address(referenceConduit), address(this), true + ); + vm.label(address(testZone), "TestZone"); + } + + function testCalldataEquivalence(TestCase memory testCase) public { + // test( + // this.execCalldataEquivalence, + // Context({ + // consideration: consideration, + // testCase: testCase + // }) + // ); + test( + this.execCalldataEquivalence, + Context({ + consideration: referenceConsideration, + testCase: testCase + }) + ); + } + + function execCalldataEquivalence(Context memory context) public stateless { + // Bound the test case. + TestCase memory testCase = _boundTestCase(context.testCase); + + // Mint the necessary tokens. + _mintNecessaryTokens(testCase); + + // Create the params for the advanced order. + OrderParameters memory orderParameters = OrderParameters({ + offerer: address(this), + zone: address(testZone), + offer: new OfferItem[](testCase.offerLength), + consideration: new ConsiderationItem[](testCase.considerationLength), + orderType: OrderType(testCase.orderType), + startTime: testCase.startTime, + endTime: testCase.endTime, + zoneHash: bytes32(testCase.zoneHash), + salt: testCase.salt, + conduitKey: bytes32(0), + totalOriginalConsiderationItems: testCase.considerationLength + }); + + // Populate the offer and consideration. + for (uint256 i = 0; i < testCase.offerLength; i++) { + orderParameters.offer[i] = OfferItem({ + itemType: ItemType(testCase.itemType), + token: address(test721_1), + identifierOrCriteria: testCase.offerItemStartingIdentifier + i, + startAmount: testCase.startAmount, + endAmount: testCase.endAmount + }); + } + + for (uint256 i = 0; i < testCase.considerationLength; i++) { + orderParameters.consideration[i] = ConsiderationItem({ + itemType: ItemType(testCase.itemType), + token: address(test721_1), + identifierOrCriteria: testCase.considerationItemStartingIdentifier + i, + startAmount: testCase.startAmount, + endAmount: testCase.endAmount, + recipient: payable(address(uint160(testCase.recipient))) + }); + } + + // Create the advanced order. + AdvancedOrder memory advancedOrder = AdvancedOrder({ + parameters: orderParameters, + numerator: 1, + denominator: 1, + signature: new bytes(0), + extraData: abi.encodePacked(bytes32(testCase.extraData)) + }); + + // Generate the order hash. + bytes32 orderHash = context.consideration.getOrderHash( + advancedOrder.parameters.toOrderComponents(0) + ); + + // Create the expected zone parameters. + ZoneParameters memory zoneParameters = ZoneParameters({ + orderHash: orderHash, + fulfiller: address(this), + offerer: address(this), + offer: orderParameters.offer.toSpentItemArray(), + consideration: orderParameters.consideration.toReceivedItemArray(), + extraData: abi.encodePacked(bytes32(testCase.extraData)), + orderHashes: new bytes32[](0), + startTime: testCase.startTime, + endTime: testCase.endTime, + zoneHash: bytes32(testCase.zoneHash) + }); + + // Hash the zone parameters. + bytes32 expectedZoneHash = bytes32( + keccak256(abi.encode(zoneParameters)) + ); + + // Send the expectation for authorize to the test zone. + testZone.setExpectedAuthorizeCalldataHash(expectedZoneHash); + + // Add the order hash to the zone parameters. + zoneParameters.orderHashes = new bytes32[](1); + zoneParameters.orderHashes[0] = orderHash; + + // Hash the updated zone parameters. + expectedZoneHash = bytes32( + keccak256(abi.encode(zoneParameters)) + ); + + // Send the expectation for validate to the test zone. + testZone.setExpectedValidateCalldataHash(expectedZoneHash); + + // Fulfill the advanced order. + context.consideration.fulfillAdvancedOrder({ + advancedOrder: advancedOrder, + criteriaResolvers: new CriteriaResolver[](0), + fulfillerConduitKey: bytes32(0), + recipient: payable(address(uint160(testCase.recipient))) + }); + } + + function _boundTestCase(TestCase memory _testCase) internal view returns (TestCase memory) { + TestCase memory testCase = _testCase; + + testCase.itemType = bound(testCase.itemType, 2, 2); + testCase.offerItemStartingIdentifier = bound(testCase.offerItemStartingIdentifier, 1, type(uint16).max); + testCase.considerationItemStartingIdentifier = bound(testCase.considerationItemStartingIdentifier, type(uint32).max, type(uint64).max); + testCase.startAmount = bound(testCase.startAmount, 1, 1); + testCase.endAmount = bound(testCase.endAmount, 1, testCase.itemType == 2 ? 1 : 1000); + testCase.recipient = bound(testCase.recipient, 10, type(uint160).max); + testCase.offerLength = bound(testCase.offerLength, 1, 30); + testCase.considerationLength = bound(testCase.considerationLength, 1, 30); + testCase.orderType = bound(testCase.orderType, 1, 3); // 0, 4); + testCase.startTime = bound(testCase.startTime, 0, 1); + testCase.endTime = bound(testCase.endTime, block.timestamp + 1, type(uint256).max); + + return testCase; + } + + function _mintNecessaryTokens(TestCase memory testCase) internal { + for (uint256 i = 0; i < testCase.offerLength; i++) { + test721_1.mint(address(this), testCase.offerItemStartingIdentifier + i); + test1155_1.mint(address(this), testCase.offerItemStartingIdentifier + i, testCase.endAmount); + } + for (uint256 i = 0; i < testCase.considerationLength; i++) { + test721_1.mint(address(this), testCase.considerationItemStartingIdentifier + i); + test1155_1.mint(address(this), testCase.considerationItemStartingIdentifier + i, testCase.endAmount); + } + } +} diff --git a/test/foundry/zone/impl/HashCalldataTestZone.sol b/test/foundry/zone/impl/HashCalldataTestZone.sol new file mode 100644 index 0000000..315ba76 --- /dev/null +++ b/test/foundry/zone/impl/HashCalldataTestZone.sol @@ -0,0 +1,113 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { + Schema, + ZoneParameters +} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +import { ZoneInterface } from + "seaport-types/src/interfaces/ZoneInterface.sol"; + +contract HashCalldataTestZone is ZoneInterface { + bytes32 public expectedZoneAuthorizeCalldataHash; + bytes32 public expectedZoneValidateCalldataHash; + + function authorizeOrder(ZoneParameters calldata zoneParameters) + public + view + returns (bytes4) + { + // Hash the zone parameters. + bytes32 _expectedZoneHash = bytes32( + keccak256(abi.encode(zoneParameters)) + ); + + if (_expectedZoneHash != expectedZoneAuthorizeCalldataHash) { + revert( + "Zone calldata hash does not match expected zone hash in authorizeOrder" + ); + } + + // Return the authorizeOrder magic value. + return this.authorizeOrder.selector; + } + + /** + * @dev Validates the order with the given `zoneParameters`. Called by + * Consideration whenever any extraData is provided by the caller. + * + * @param zoneParameters The parameters for the order. + * + * @return validOrderMagicValue The validOrder magic value. + */ + function validateOrder(ZoneParameters calldata zoneParameters) + external + view + returns (bytes4 validOrderMagicValue) + { + // Hash the zone parameters. + bytes32 _expectedZoneHash = bytes32( + keccak256(abi.encode(zoneParameters)) + ); + + if (_expectedZoneHash != expectedZoneValidateCalldataHash) { + revert( + "Zone calldata hash does not match expected zone hash in validateOrder" + ); + } + + // Return the validOrderMagicValue. + return ZoneInterface.validateOrder.selector; + } + + function setExpectedAuthorizeCalldataHash( + bytes32 _expectedZoneCalldataHash + ) public { + expectedZoneAuthorizeCalldataHash = _expectedZoneCalldataHash; + } + + function setExpectedValidateCalldataHash( + bytes32 _expectedZoneCalldataHash + ) public { + expectedZoneValidateCalldataHash = _expectedZoneCalldataHash; + } + + receive() external payable { } + + function getSeaportMetadata() + external + pure + override(ZoneInterface) + returns (string memory name, Schema[] memory schemas) + { + // Return the metadata. + name = "TestCalldataHashContractOfferer"; + schemas = new Schema[](1); + schemas[0].id = 1337; + schemas[0].metadata = new bytes(0); + } + + /** + * @dev Enable accepting ERC1155 tokens via safeTransfer. + */ + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external pure returns (bytes4) { + return this.onERC1155Received.selector; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ZoneInterface) + returns (bool) + { + return interfaceId == type(ZoneInterface).interfaceId; + } +} diff --git a/test/foundry/zone/impl/PostFullfillmentStatefulTestZone.sol b/test/foundry/zone/impl/StatefulTestZone.sol similarity index 77% rename from test/foundry/zone/impl/PostFullfillmentStatefulTestZone.sol rename to test/foundry/zone/impl/StatefulTestZone.sol index 01eb260..f6d7b4d 100644 --- a/test/foundry/zone/impl/PostFullfillmentStatefulTestZone.sol +++ b/test/foundry/zone/impl/StatefulTestZone.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.17; import { - ZoneParameters, - Schema + Schema, + ZoneParameters } from "seaport-types/src/lib/ConsiderationStructs.sol"; import { ItemType } from "seaport-types/src/lib/ConsiderationEnums.sol"; @@ -12,7 +12,7 @@ import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { ZoneInterface } from "seaport-types/src/interfaces/ZoneInterface.sol"; -contract PostFulfillmentStatefulTestZone is ERC165, ZoneInterface { +contract StatefulTestZone is ERC165, ZoneInterface { error IncorrectAmount(uint256 actual, uint256 expected); error IncorrectItemType(ItemType actual, ItemType expected); error IncorrectIdentifier(uint256 actual, uint256 expected); @@ -23,13 +23,19 @@ contract PostFulfillmentStatefulTestZone is ERC165, ZoneInterface { amountToCheck = amount; } - bool public called = false; + bool public authorizeCalled = false; + bool public validateCalled = false; - function authorizeOrder(ZoneParameters calldata) + function authorizeOrder(ZoneParameters calldata zoneParameters) public - pure returns (bytes4) { + _checkZoneParameters(zoneParameters); + + // Set the global called flag to true. + authorizeCalled = true; + + // Return the authorizeOrder magic value. return this.authorizeOrder.selector; } @@ -45,30 +51,10 @@ contract PostFulfillmentStatefulTestZone is ERC165, ZoneInterface { external returns (bytes4 validOrderMagicValue) { - // Check that the amount in the offer is correct. - if (zoneParameters.offer[0].amount != amountToCheck) { - revert IncorrectAmount( - zoneParameters.offer[0].amount, amountToCheck - ); - } - - // Check that the item type in the consideration is correct. - if (zoneParameters.consideration[0].itemType != ItemType.ERC721) { - revert IncorrectIdentifier( - uint256(zoneParameters.consideration[0].itemType), - uint256(ItemType.ERC721) - ); - } - - // Check that the token ID in the consideration is correct. - if (zoneParameters.consideration[0].identifier != 42) { - revert IncorrectIdentifier( - zoneParameters.consideration[0].identifier, 42 - ); - } + _checkZoneParameters(zoneParameters); // Set the global called flag to true. - called = true; + validateCalled = true; // Return the validOrderMagicValue. return ZoneInterface.validateOrder.selector; @@ -90,7 +76,7 @@ contract PostFulfillmentStatefulTestZone is ERC165, ZoneInterface { schemas[0].id = 3003; schemas[0].metadata = new bytes(0); - return ("PostFulfillmentStatefulTestZone", schemas); + return ("StatefulTestZone", schemas); } function supportsInterface(bytes4 interfaceId) @@ -102,4 +88,30 @@ contract PostFulfillmentStatefulTestZone is ERC165, ZoneInterface { return interfaceId == type(ZoneInterface).interfaceId || super.supportsInterface(interfaceId); } + + function _checkZoneParameters(ZoneParameters calldata zoneParameters) + internal + view + { + // Check that the amount in the offer is correct. + if (zoneParameters.offer[0].amount != amountToCheck) { + revert IncorrectAmount( + zoneParameters.offer[0].amount, amountToCheck + ); + } + + // Check that the item type in the consideration is correct. + if (zoneParameters.consideration[0].itemType != ItemType.ERC721) { + revert IncorrectItemType( + zoneParameters.consideration[0].itemType, ItemType.ERC721 + ); + } + + // Check that the token ID in the consideration is correct. + if (zoneParameters.consideration[0].identifier != 42) { + revert IncorrectIdentifier( + zoneParameters.consideration[0].identifier, 42 + ); + } + } }