diff --git a/.github/workflows/ci-default.yml b/.github/workflows/ci-default.yml index 7c338fa1..9febf105 100644 --- a/.github/workflows/ci-default.yml +++ b/.github/workflows/ci-default.yml @@ -21,11 +21,6 @@ env: on: # A push occurs to one of the matched branches. push: - pull_request: - branches: - - main - - develop - - develop-contracts # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/contracts/.solhint.json b/contracts/.solhint.json index 5a2b49ec..c4ea5b00 100644 --- a/contracts/.solhint.json +++ b/contracts/.solhint.json @@ -8,7 +8,7 @@ "no-console": "off", "no-empty-blocks": "off", "not-rely-on-time": "off", - "private-vars-leading-underscore": "warn", + "private-vars-leading-underscore": ["off", { "strict": false }], "reason-string": ["warn", { "maxLength": 64 }], "one-contract-per-file": "off" } diff --git a/contracts/remappings.txt b/contracts/remappings.txt index 6fdc5ec2..0134abce 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -9,3 +9,4 @@ prb-test/=lib/prb-test/src/ @looksrare/=node_modules/@looksrare/ hardhat/=node_modules/hardhat/ solmate/=node_modules/solmate/ +@openzeppelin/=node_modules/@openzeppelin/ \ No newline at end of file diff --git a/contracts/src/marketplace/LooksRareProtocol.sol b/contracts/src/marketplace/LooksRareProtocol.sol index b60e6e30..36302ff5 100644 --- a/contracts/src/marketplace/LooksRareProtocol.sol +++ b/contracts/src/marketplace/LooksRareProtocol.sol @@ -79,7 +79,7 @@ import {QuoteType} from "./enums/QuoteType.sol"; * ~~~ ~~~ * ~~~~ ~~~~ * ~~~~~~ - * @author LooksRare protocol team (👀,💎) + * @author LooksRare protocol team (👀,💎); bitbeckers */ contract LooksRareProtocol is ILooksRareProtocol, diff --git a/contracts/src/marketplace/TransferManager.sol b/contracts/src/marketplace/TransferManager.sol index cc1c9578..74bcf20f 100644 --- a/contracts/src/marketplace/TransferManager.sol +++ b/contracts/src/marketplace/TransferManager.sol @@ -239,26 +239,11 @@ contract TransferManager is ITransferManager, LowLevelERC721Transfer, LowLevelER CollectionType collectionType = items[i].collectionType; if (collectionType == CollectionType.ERC721) { - for (uint256 j; j < itemIdsLengthForSingleCollection;) { - if (amounts[j] != 1) { - revert AmountInvalid(); - } - _executeERC721TransferFrom(items[i].collection, from, to, itemIds[j]); - unchecked { - ++j; - } - } + _processBatch721Collection(items[i], from, to); } else if (collectionType == CollectionType.ERC1155) { - for (uint256 j; j < itemIdsLengthForSingleCollection;) { - if (amounts[j] == 0) { - revert AmountInvalid(); - } - - unchecked { - ++j; - } - } - _executeERC1155SafeBatchTransferFrom(items[i].collection, from, to, itemIds, amounts); + _processBatch1155Collection(items[i], from, to); + } else if (collectionType == CollectionType.Hypercert) { + _processBatchHypercertCollection(items[i], from, to); } unchecked { @@ -372,4 +357,56 @@ contract TransferManager is ITransferManager, LowLevelERC721Transfer, LowLevelER revert TransferCallerInvalid(); } + + function _processBatch721Collection(BatchTransferItem calldata batchItems, address from, address to) private { + uint256[] calldata itemIds = batchItems.itemIds; + uint256[] calldata amounts = batchItems.amounts; + uint256 length = itemIds.length; + + for (uint256 j; j < length;) { + if (amounts[j] != 1) { + revert AmountInvalid(); + } + _executeERC721TransferFrom(batchItems.collection, from, to, itemIds[j]); + unchecked { + ++j; + } + } + } + + function _processBatch1155Collection(BatchTransferItem calldata batchItems, address from, address to) private { + uint256[] calldata itemIds = batchItems.itemIds; + uint256[] calldata amounts = batchItems.amounts; + uint256 length = itemIds.length; + + for (uint256 j; j < length;) { + if (amounts[j] == 0) { + revert AmountInvalid(); + } + + unchecked { + ++j; + } + } + _executeERC1155SafeBatchTransferFrom(batchItems.collection, from, to, itemIds, amounts); + } + + function _processBatchHypercertCollection(BatchTransferItem calldata batchItems, address from, address to) + private + { + uint256[] calldata itemIds = batchItems.itemIds; + uint256[] calldata amounts = batchItems.amounts; + uint256 length = itemIds.length; + + for (uint256 j; j < length;) { + if (amounts[j] == 0) { + revert AmountInvalid(); + } + + unchecked { + ++j; + } + } + _executeERC1155SafeBatchTransferFrom(batchItems.collection, from, to, itemIds, amounts); + } } diff --git a/contracts/src/marketplace/TransferSelectorNFT.sol b/contracts/src/marketplace/TransferSelectorNFT.sol index 8b4d201c..45869668 100644 --- a/contracts/src/marketplace/TransferSelectorNFT.sol +++ b/contracts/src/marketplace/TransferSelectorNFT.sol @@ -59,7 +59,7 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard { } else if (collectionType == CollectionType.ERC1155) { transferManager.transferItemsERC1155(collection, sender, recipient, itemIds, amounts); } else if (collectionType == CollectionType.Hypercert) { - transferManager.transferItemsERC1155(collection, sender, recipient, itemIds, amounts); + transferManager.transferItemsHypercert(collection, sender, recipient, itemIds, amounts); } else if (collectionType == CollectionType.Hyperboard) { revert UnsupportedCollectionType(); } diff --git a/contracts/src/marketplace/interfaces/ITransferManager.sol b/contracts/src/marketplace/interfaces/ITransferManager.sol index 8f19f9a4..77745be1 100644 --- a/contracts/src/marketplace/interfaces/ITransferManager.sol +++ b/contracts/src/marketplace/interfaces/ITransferManager.sol @@ -15,7 +15,7 @@ interface ITransferManager { /** * @notice This struct is only used for transferBatchItemsAcrossCollections. * @param collection Collection address - * @param collectionType 0 for ERC721, 1 for ERC1155 + * @param collectionType 0 for ERC721, 1 for ERC1155, 2 for Hypercert, 3 for Hyperboard * @param itemIds Array of item ids to transfer * @param amounts Array of amounts to transfer */ diff --git a/contracts/test/foundry/marketplace/ProtocolBase.t.sol b/contracts/test/foundry/marketplace/ProtocolBase.t.sol index 8c3cee4b..e0af4b8e 100644 --- a/contracts/test/foundry/marketplace/ProtocolBase.t.sol +++ b/contracts/test/foundry/marketplace/ProtocolBase.t.sol @@ -11,6 +11,8 @@ import {OrderStructs} from "@hypercerts/marketplace/libraries/OrderStructs.sol"; import {LooksRareProtocol, ILooksRareProtocol} from "@hypercerts/marketplace/LooksRareProtocol.sol"; import {TransferManager} from "@hypercerts/marketplace/TransferManager.sol"; import {ProtocolFeeRecipient} from "@hypercerts/marketplace/ProtocolFeeRecipient.sol"; +import {HypercertMinter} from "@hypercerts/protocol/HypercertMinter.sol"; +import {IHypercertToken} from "@hypercerts/protocol/interfaces/IHypercertToken.sol"; // Other contracts import {OrderValidatorV2A} from "@hypercerts/marketplace/helpers/OrderValidatorV2A.sol"; @@ -21,6 +23,7 @@ import {MockERC721} from "../../mock/MockERC721.sol"; import {MockERC721WithRoyalties} from "../../mock/MockERC721WithRoyalties.sol"; import {MockERC1155} from "../../mock/MockERC1155.sol"; import {MockRoyaltyFeeRegistry} from "../../mock/MockRoyaltyFeeRegistry.sol"; +import {MockHypercertMinterUUPS} from "../../mock/MockHypercertMinterUUPS.sol"; // Utils import {MockOrderGenerator} from "./utils/MockOrderGenerator.sol"; @@ -32,6 +35,11 @@ contract ProtocolBase is MockOrderGenerator, ILooksRareProtocol { MockERC721WithRoyalties public mockERC721WithRoyalties; MockERC721 public mockERC721; MockERC1155 public mockERC1155; + MockHypercertMinterUUPS public mockHypercertMinterUUPS; + HypercertMinter public mockHypercertMinter; + + IHypercertToken.TransferRestrictions public constant FROM_CREATOR_ONLY = + IHypercertToken.TransferRestrictions.FromCreatorOnly; ProtocolFeeRecipient public protocolFeeRecipient; LooksRareProtocol public looksRareProtocol; @@ -125,6 +133,8 @@ contract ProtocolBase is MockOrderGenerator, ILooksRareProtocol { mockERC721.setApprovalForAll(address(transferManager), true); mockERC1155.setApprovalForAll(address(transferManager), true); mockERC721WithRoyalties.setApprovalForAll(address(transferManager), true); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); + weth.approve(address(looksRareProtocol), type(uint256).max); // Grant approvals for transfer manager @@ -153,6 +163,9 @@ contract ProtocolBase is MockOrderGenerator, ILooksRareProtocol { looksRareToken = new MockERC20(); mockERC721 = new MockERC721(); mockERC1155 = new MockERC1155(); + mockHypercertMinterUUPS = new MockHypercertMinterUUPS(); + mockHypercertMinterUUPS.setUp(); + mockHypercertMinter = mockHypercertMinterUUPS.minter(); transferManager = new TransferManager(_owner); royaltyFeeRegistry = new MockRoyaltyFeeRegistry(_owner, 9500); diff --git a/contracts/test/foundry/marketplace/SignaturesERC1271WalletForHypercert.t.sol b/contracts/test/foundry/marketplace/SignaturesERC1271WalletForHypercert.t.sol new file mode 100644 index 00000000..d2bac768 --- /dev/null +++ b/contracts/test/foundry/marketplace/SignaturesERC1271WalletForHypercert.t.sol @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +// Libraries and interfaces +import {IReentrancyGuard} from "@looksrare/contracts-libs/contracts/interfaces/IReentrancyGuard.sol"; +import {OrderStructs} from "@hypercerts/marketplace/libraries/OrderStructs.sol"; + +// Base test +import {ProtocolBase} from "./ProtocolBase.t.sol"; + +// Mocks and other utils +import {ERC1271Wallet} from "./utils/ERC1271Wallet.sol"; +import {MaliciousERC1271Wallet} from "./utils/MaliciousERC1271Wallet.sol"; +import {MaliciousOnERC1155ReceivedERC1271Wallet} from "./utils/MaliciousOnERC1155ReceivedERC1271Wallet.sol"; +import {MaliciousOnERC1155ReceivedTheThirdTimeERC1271Wallet} from + "./utils/MaliciousOnERC1155ReceivedTheThirdTimeERC1271Wallet.sol"; +import {MaliciousIsValidSignatureERC1271Wallet} from "./utils/MaliciousIsValidSignatureERC1271Wallet.sol"; + +// Errors +import {SignatureERC1271Invalid} from "@looksrare/contracts-libs/contracts/errors/SignatureCheckerErrors.sol"; +import { + ERC1155SafeTransferFromFail, + ERC1155SafeBatchTransferFromFail +} from "@looksrare/contracts-libs/contracts/errors/LowLevelErrors.sol"; +import {SIGNATURE_INVALID_EIP1271} from "@hypercerts/marketplace/constants/ValidationCodeConstants.sol"; + +// Enums +import {CollectionType} from "@hypercerts/marketplace/enums/CollectionType.sol"; +import {QuoteType} from "@hypercerts/marketplace/enums/QuoteType.sol"; + +/** + * @dev ERC1271Wallet recovers a signature's signer using ECDSA. If it matches the mock wallet's + * owner, it returns the magic value. Otherwise it returns an empty bytes4 value. + */ +contract SignaturesERC1271WalletForHypercertTest is ProtocolBase { + uint256 private constant price = 1 ether; // Fixed price of sale + uint256 private constant fractionId = (1 << 128) + 1; + bytes private constant _EMPTY_SIGNATURE = new bytes(0); + + function setUp() public { + _setUp(); + _setUpUser(takerUser); + _setupRegistryRoyalties(address(mockHypercertMinter), _standardRoyaltyFee); + } + + function testTakerBid() public { + ERC1271Wallet wallet = new ERC1271Wallet(address(makerUser)); + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = _takerBidSetup(address(wallet)); + + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + vm.startPrank(address(wallet)); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); + transferManager.grantApprovals(operators); + vm.stopPrank(); + + _assertValidMakerOrder(makerAsk, signature); + + vm.prank(takerUser); + looksRareProtocol.executeTakerBid{value: price}(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + assertEq(mockHypercertMinter.balanceOf(takerUser, fractionId), 1); + } + + function testTakerBidInvalidSignature() public { + ERC1271Wallet wallet = new ERC1271Wallet(address(makerUser)); + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = _takerBidSetup(address(wallet)); + + // Signed by a different private key + bytes memory signature = _signMakerOrder(makerAsk, takerUserPK); + + vm.startPrank(address(wallet)); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); + transferManager.grantApprovals(operators); + vm.stopPrank(); + + _assertMakerOrderReturnValidationCode(makerAsk, signature, SIGNATURE_INVALID_EIP1271); + + vm.expectRevert(SignatureERC1271Invalid.selector); + vm.prank(takerUser); + looksRareProtocol.executeTakerBid{value: price}(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + } + + function testTakerBidIsInvalidSignatureReentrancy() public { + MaliciousIsValidSignatureERC1271Wallet maliciousERC1271Wallet = new MaliciousIsValidSignatureERC1271Wallet( + address(looksRareProtocol) + ); + _setUpUser(address(maliciousERC1271Wallet)); + maliciousERC1271Wallet.setFunctionToReenter(MaliciousERC1271Wallet.FunctionToReenter.ExecuteTakerBid); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _takerBidSetup(address(maliciousERC1271Wallet)); + + vm.expectRevert(IReentrancyGuard.ReentrancyFail.selector); + vm.prank(takerUser); + looksRareProtocol.executeTakerBid{value: price}(takerBid, makerAsk, _EMPTY_SIGNATURE, _EMPTY_MERKLE_TREE); + } + + function testTakerAsk() public { + ERC1271Wallet wallet = new ERC1271Wallet(address(makerUser)); + (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) = _takerAskSetup(address(wallet)); + + bytes memory signature = _signMakerOrder(makerBid, makerUserPK); + + // Wallet needs to hold WETH and have given WETH approval + deal(address(weth), address(wallet), price); + vm.prank(address(wallet)); + weth.approve(address(looksRareProtocol), price); + + _assertValidMakerOrder(makerBid, signature); + + vm.prank(takerUser); + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE); + + assertEq(mockHypercertMinter.balanceOf(address(wallet), fractionId), 1); + } + + function testTakerAskInvalidSignature() public { + ERC1271Wallet wallet = new ERC1271Wallet(address(makerUser)); + (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) = _takerAskSetup(address(wallet)); + + // Signed by a different private key + bytes memory signature = _signMakerOrder(makerBid, takerUserPK); + + // Wallet needs to hold WETH and have given WETH approval + deal(address(weth), address(wallet), price); + vm.prank(address(wallet)); + weth.approve(address(looksRareProtocol), price); + + _assertMakerOrderReturnValidationCode(makerBid, signature, SIGNATURE_INVALID_EIP1271); + + vm.expectRevert(SignatureERC1271Invalid.selector); + vm.prank(takerUser); + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE); + } + + function testTakerAskIsValidSignatureReentrancy() public { + MaliciousIsValidSignatureERC1271Wallet maliciousERC1271Wallet = new MaliciousIsValidSignatureERC1271Wallet( + address(looksRareProtocol) + ); + _setUpUser(address(maliciousERC1271Wallet)); + maliciousERC1271Wallet.setFunctionToReenter(MaliciousERC1271Wallet.FunctionToReenter.ExecuteTakerAsk); + + (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) = + _takerAskSetup(address(maliciousERC1271Wallet)); + + vm.expectRevert(IReentrancyGuard.ReentrancyFail.selector); + vm.prank(takerUser); + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, _EMPTY_SIGNATURE, _EMPTY_MERKLE_TREE); + } + + function testTakerAskOnERC1155ReceivedReentrancy() public { + MaliciousOnERC1155ReceivedERC1271Wallet maliciousERC1271Wallet = new MaliciousOnERC1155ReceivedERC1271Wallet( + address(looksRareProtocol) + ); + _setUpUser(address(maliciousERC1271Wallet)); + + (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) = + _takerAskSetup(address(maliciousERC1271Wallet)); + + maliciousERC1271Wallet.setFunctionToReenter(MaliciousERC1271Wallet.FunctionToReenter.ExecuteTakerAsk); + + vm.expectRevert(ERC1155SafeTransferFromFail.selector); + vm.prank(takerUser); + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, _EMPTY_SIGNATURE, _EMPTY_MERKLE_TREE); + } + + function testBatchTakerAsk() public { + ERC1271Wallet wallet = new ERC1271Wallet(makerUser); + (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) = _batchTakerAskSetup(address(wallet)); + + bytes memory signature = _signMakerOrder(makerBid, makerUserPK); + + // Wallet needs to hold WETH and have given WETH approval + deal(address(weth), address(wallet), price); + vm.prank(address(wallet)); + weth.approve(address(looksRareProtocol), price); + + vm.prank(takerUser); + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE); + + for (uint256 i; i < 10; i++) { + assertEq(mockHypercertMinter.balanceOf(address(wallet), (((1 + i) << 128) + 1)), 1); + } + } + + function testBatchTakerAskOnERC1155BatchReceivedReentrancy() public { + MaliciousOnERC1155ReceivedERC1271Wallet maliciousERC1271Wallet = new MaliciousOnERC1155ReceivedERC1271Wallet( + address(looksRareProtocol) + ); + _setUpUser(address(maliciousERC1271Wallet)); + + (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) = + _batchTakerAskSetup(address(maliciousERC1271Wallet)); + + bytes memory signature = _signMakerOrder(makerBid, makerUserPK); + + // Wallet needs to hold WETH and have given WETH approval + deal(address(weth), address(maliciousERC1271Wallet), price); + vm.prank(address(maliciousERC1271Wallet)); + weth.approve(address(looksRareProtocol), price); + + maliciousERC1271Wallet.setFunctionToReenter(MaliciousERC1271Wallet.FunctionToReenter.ExecuteTakerAsk); + + vm.expectRevert(ERC1155SafeBatchTransferFromFail.selector); + vm.prank(takerUser); + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE); + } + + uint256 private constant numberOfPurchases = 3; + + function testExecuteMultipleTakerBids() public { + ERC1271Wallet wallet = new ERC1271Wallet(address(makerUser)); + + ( + OrderStructs.Maker[] memory makerAsks, + OrderStructs.Taker[] memory takerBids, + OrderStructs.MerkleTree[] memory merkleTrees, + bytes[] memory signatures + ) = _multipleTakerBidsSetup(address(wallet)); + + vm.startPrank(address(wallet)); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); + transferManager.grantApprovals(operators); + vm.stopPrank(); + + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberOfPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + + for (uint256 i; i < numberOfPurchases; i++) { + assertEq(mockHypercertMinter.balanceOf(takerUser, (((1 + i) << 128) + 1)), 1); + } + } + + function testExecuteMultipleTakerBidsInvalidSignatures() public { + ERC1271Wallet wallet = new ERC1271Wallet(address(makerUser)); + + ( + OrderStructs.Maker[] memory makerAsks, + OrderStructs.Taker[] memory takerBids, + OrderStructs.MerkleTree[] memory merkleTrees, + bytes[] memory signatures + ) = _multipleTakerBidsSetup(address(wallet)); + + // Signed by a different private key + for (uint256 i; i < signatures.length; i++) { + signatures[i] = _signMakerOrder(makerAsks[i], takerUserPK); + } + + vm.startPrank(address(wallet)); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); + transferManager.grantApprovals(operators); + vm.stopPrank(); + + vm.expectRevert(SignatureERC1271Invalid.selector); + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberOfPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + } + + function testExecuteMultipleTakerBidsIsValidSignatureReentrancy() public { + MaliciousIsValidSignatureERC1271Wallet maliciousERC1271Wallet = new MaliciousIsValidSignatureERC1271Wallet( + address(looksRareProtocol) + ); + _setUpUser(address(maliciousERC1271Wallet)); + maliciousERC1271Wallet.setFunctionToReenter(MaliciousERC1271Wallet.FunctionToReenter.ExecuteMultipleTakerBids); + + ( + OrderStructs.Maker[] memory makerAsks, + OrderStructs.Taker[] memory takerBids, + OrderStructs.MerkleTree[] memory merkleTrees, + bytes[] memory signatures + ) = _multipleTakerBidsSetup(address(maliciousERC1271Wallet)); + + vm.expectRevert(IReentrancyGuard.ReentrancyFail.selector); + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberOfPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + + for (uint256 i; i < numberOfPurchases - 1; i++) { + assertEq(mockHypercertMinter.balanceOf(takerUser, i), 0); + } + } + + function testExecuteMultipleTakerBidsOnERC1155ReceivedReentrancyOnlyInTheLastCall() public { + MaliciousOnERC1155ReceivedTheThirdTimeERC1271Wallet maliciousERC1271Wallet = + new MaliciousOnERC1155ReceivedTheThirdTimeERC1271Wallet( + takerUser + ); + _setUpUser(makerUser); + maliciousERC1271Wallet.setFunctionToReenter(MaliciousERC1271Wallet.FunctionToReenter.ExecuteMultipleTakerBids); + + ( + OrderStructs.Maker[] memory makerAsks, + OrderStructs.Taker[] memory takerBids, + OrderStructs.MerkleTree[] memory merkleTrees, + bytes[] memory signatures + ) = _multipleTakerBidsSetup(makerUser); + + // Set the NFT recipient as the ERC1271 wallet to trigger onERC1155Received + for (uint256 i; i < numberOfPurchases; i++) { + takerBids[i].recipient = address(maliciousERC1271Wallet); + } + + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberOfPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + + // First 2 purchases should go through, but the last one fails silently + for (uint256 i; i < numberOfPurchases - 1; i++) { + assertEq(mockHypercertMinter.balanceOf(address(maliciousERC1271Wallet), (((1 + i) << 128) + 1)), 1); + } + assertEq(mockHypercertMinter.balanceOf(address(maliciousERC1271Wallet), numberOfPurchases - 1), 0); + } + + function _takerBidSetup(address signer) + private + returns (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) + { + // Mint asset + vm.prank(signer); + mockHypercertMinter.mintClaim(signer, 10_000, "http://example.com/takerBid", FROM_CREATOR_ONLY); + + makerAsk = _createSingleItemMakerOrder({ + quoteType: QuoteType.Ask, + globalNonce: 0, + subsetNonce: 0, + strategyId: STANDARD_SALE_FOR_FIXED_PRICE_STRATEGY, + collectionType: CollectionType.Hypercert, + orderNonce: 0, + collection: address(mockHypercertMinter), + currency: ETH, + signer: signer, + price: price, + itemId: fractionId + }); + + // Prepare the taker bid + takerBid = _genericTakerOrder(); + } + + function _takerAskSetup(address signer) + private + returns (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) + { + makerBid = _createSingleItemMakerOrder({ + quoteType: QuoteType.Bid, + globalNonce: 0, + subsetNonce: 0, + strategyId: STANDARD_SALE_FOR_FIXED_PRICE_STRATEGY, + collectionType: CollectionType.Hypercert, + orderNonce: 0, + collection: address(mockHypercertMinter), + currency: address(weth), + signer: signer, + price: price, + itemId: fractionId + }); + + // Mint asset + vm.prank(takerUser); + mockHypercertMinter.mintClaim(takerUser, 10_000, "http://example.com/takerBid", FROM_CREATOR_ONLY); + + // Prepare the taker ask + takerAsk = _genericTakerOrder(); + } + + function _batchTakerAskSetup(address signer) + private + returns (OrderStructs.Taker memory takerAsk, OrderStructs.Maker memory makerBid) + { + uint256[] memory itemIds = new uint256[](10); + uint256[] memory amounts = new uint256[](10); + + vm.startPrank(takerUser); + for (uint256 i; i < 10; i++) { + uint256 claimID = (1 + i) << 128; + itemIds[i] = claimID + 1; + amounts[i] = 1; + + // Mint asset + mockHypercertMinter.mintClaim(takerUser, 10_000, "http://example.com/takerBid", FROM_CREATOR_ONLY); + } + vm.stopPrank(); + + // Prepare the first order + makerBid = _createMultiItemMakerOrder({ + quoteType: QuoteType.Bid, + globalNonce: 0, + subsetNonce: 0, + strategyId: STANDARD_SALE_FOR_FIXED_PRICE_STRATEGY, + collectionType: CollectionType.Hypercert, + orderNonce: 0, + collection: address(mockHypercertMinter), + currency: address(weth), + signer: signer, + price: price, + itemIds: itemIds, + amounts: amounts + }); + + // Prepare the taker ask + takerAsk = _genericTakerOrder(); + } + + function _multipleTakerBidsSetup(address signer) + private + returns ( + OrderStructs.Maker[] memory makerAsks, + OrderStructs.Taker[] memory takerBids, + OrderStructs.MerkleTree[] memory merkleTrees, + bytes[] memory signatures + ) + { + makerAsks = new OrderStructs.Maker[](numberOfPurchases); + takerBids = new OrderStructs.Taker[](numberOfPurchases); + signatures = new bytes[](numberOfPurchases); + + vm.startPrank(signer); + for (uint256 i; i < numberOfPurchases; i++) { + // Mint asset + mockHypercertMinter.mintClaim(signer, 10_000, "http://example.com/takerBid", FROM_CREATOR_ONLY); + + uint256 fractionID = ((1 + i) << 128) + 1; + + makerAsks[i] = _createSingleItemMakerOrder({ + quoteType: QuoteType.Ask, + globalNonce: 0, + subsetNonce: 0, + strategyId: STANDARD_SALE_FOR_FIXED_PRICE_STRATEGY, + collectionType: CollectionType.ERC1155, + orderNonce: i, + collection: address(mockHypercertMinter), + currency: ETH, + signer: signer, + price: price, + itemId: fractionID // 0, 1, etc. + }); + + signatures[i] = _signMakerOrder(makerAsks[i], makerUserPK); + + takerBids[i] = _genericTakerOrder(); + } + vm.stopPrank(); + + // Other execution parameters + merkleTrees = new OrderStructs.MerkleTree[](numberOfPurchases); + } +} diff --git a/contracts/test/foundry/marketplace/StandardTransactions.t.sol b/contracts/test/foundry/marketplace/StandardTransactions.t.sol index 6d5d8b38..75cd55d8 100644 --- a/contracts/test/foundry/marketplace/StandardTransactions.t.sol +++ b/contracts/test/foundry/marketplace/StandardTransactions.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; // Libraries, interfaces, errors import {OrderStructs} from "@hypercerts/marketplace/libraries/OrderStructs.sol"; diff --git a/contracts/test/foundry/marketplace/StandardTransactionsHypercerts.t.sol b/contracts/test/foundry/marketplace/StandardTransactionsHypercerts.t.sol new file mode 100644 index 00000000..199d24c0 --- /dev/null +++ b/contracts/test/foundry/marketplace/StandardTransactionsHypercerts.t.sol @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// Libraries, interfaces, errors +import {OrderStructs} from "@hypercerts/marketplace/libraries/OrderStructs.sol"; +import {LengthsInvalid} from "@hypercerts/marketplace/errors/SharedErrors.sol"; + +import {CreatorFeeManagerWithRoyalties} from "@hypercerts/marketplace/CreatorFeeManagerWithRoyalties.sol"; + +// Base test +import {ProtocolBase} from "./ProtocolBase.t.sol"; + +// Constants +import {ONE_HUNDRED_PERCENT_IN_BP} from "@hypercerts/marketplace/constants/NumericConstants.sol"; + +// Enums +import {CollectionType} from "@hypercerts/marketplace/enums/CollectionType.sol"; +import {QuoteType} from "@hypercerts/marketplace/enums/QuoteType.sol"; + +contract StandardTransactionsHypercertsTest is ProtocolBase { + error ERC1155SafeTransferFromFail(); + + uint256 private constant fractionId = (1 << 128) + 1; + uint256[] private fractionUnits; + uint16 private constant NEW_ROYALTY_FEE = uint16(50); + + function setUp() public { + _setUp(); + CreatorFeeManagerWithRoyalties creatorFeeManager = new CreatorFeeManagerWithRoyalties( + address(royaltyFeeRegistry) + ); + vm.prank(_owner); + looksRareProtocol.updateCreatorFeeManager(address(creatorFeeManager)); + + fractionUnits = new uint256[](3); + fractionUnits[0] = 5000; + fractionUnits[1] = 3000; + fractionUnits[2] = 2000; + } + + /** + * One Hypercert fraction (where royalties come from the registry) is sold through a taker bid + */ + function testTakerBidHypercertWithRoyaltiesFromRegistry(uint256 price) public { + vm.assume(price <= 2 ether); + _setUpUsers(); + _setupRegistryRoyalties(address(mockHypercertMinter), NEW_ROYALTY_FEE); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMockMakerAskAndTakerBid(address(mockHypercertMinter), true); + makerAsk.price = price; + + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Mint asset + vm.prank(makerUser); + mockHypercertMinter.mintClaim(makerUser, 10_000, "https://example.com/1", FROM_CREATOR_ONLY); + + // Verify validity of maker ask order + _assertValidMakerOrder(makerAsk, signature); + + // Arrays for events + uint256[3] memory expectedFees = _calculateExpectedFees({price: price, royaltyFeeBp: NEW_ROYALTY_FEE}); + address[2] memory expectedRecipients; + + expectedRecipients[0] = makerUser; + expectedRecipients[1] = _royaltyRecipient; + + // Execute taker bid transaction + vm.prank(takerUser); + vm.expectEmit(true, false, false, true); + + emit TakerBid( + NonceInvalidationParameters({ + orderHash: _computeOrderHash(makerAsk), + orderNonce: makerAsk.orderNonce, + isNonceInvalidated: true + }), + takerUser, + takerUser, + makerAsk.strategyId, + makerAsk.currency, + makerAsk.collection, + makerAsk.itemIds, + makerAsk.amounts, + expectedRecipients, + expectedFees + ); + + looksRareProtocol.executeTakerBid{value: price}(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulExecutionThroughETH(takerUser, makerUser, price, expectedFees); + + // No leftover in the balance of the contract + assertEq(address(looksRareProtocol).balance, 0); + // Verify the nonce is marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, makerAsk.orderNonce), MAGIC_VALUE_ORDER_NONCE_EXECUTED); + } + + /** + * One Hypercert fraction is sold through taker bid. Address zero is specified as the recipient in the taker struct. + */ + function testTakerBidHypercertsWithAddressZeroSpecifiedAsRecipient(uint256 price) public { + vm.assume(price <= 2 ether); + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMockMakerAskAndTakerBid(address(mockHypercertMinter), true); + makerAsk.price = price; + + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Mint asset + vm.prank(makerUser); + mockHypercertMinter.mintClaim(makerUser, 10_000, "https://example.com/1", FROM_CREATOR_ONLY); + + // Adjustment + takerBid.recipient = address(0); + + // Verify validity of maker ask order + _assertValidMakerOrder(makerAsk, signature); + + // Arrays for events + uint256[3] memory expectedFees = _calculateExpectedFees({price: price, royaltyFeeBp: 0}); + address[2] memory expectedRecipients; + + expectedRecipients[0] = makerUser; + expectedRecipients[1] = address(0); // No royalties + + // Execute taker bid transaction + vm.prank(takerUser); + vm.expectEmit(true, false, false, true); + + emit TakerBid( + NonceInvalidationParameters({ + orderHash: _computeOrderHash(makerAsk), + orderNonce: makerAsk.orderNonce, + isNonceInvalidated: true + }), + takerUser, + takerUser, + makerAsk.strategyId, + makerAsk.currency, + makerAsk.collection, + makerAsk.itemIds, + makerAsk.amounts, + expectedRecipients, + expectedFees + ); + + looksRareProtocol.executeTakerBid{value: price}(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulExecutionThroughETH(takerUser, makerUser, price, expectedFees); + } + + /** + * One Hypercert fraction (where royalties come from the registry) is sold through a taker ask using WETH + */ + function testTakerAskHypercertWithRoyaltiesFromRegistry(uint256 price) public { + vm.assume(price <= 2 ether); + + _setUpUsers(); + _setupRegistryRoyalties(address(mockHypercertMinter), NEW_ROYALTY_FEE); + + (OrderStructs.Maker memory makerBid, OrderStructs.Taker memory takerAsk) = + _createMockMakerBidAndTakerAsk(address(mockHypercertMinter), address(weth), true); + makerBid.price = price; + + bytes memory signature = _signMakerOrder(makerBid, makerUserPK); + + // Verify maker bid order + _assertValidMakerOrder(makerBid, signature); + + // Mint asset + vm.prank(takerUser); + mockHypercertMinter.mintClaim(takerUser, 10_000, "https://example.com/1", FROM_CREATOR_ONLY); + + // Arrays for events + uint256[3] memory expectedFees = _calculateExpectedFees({price: price, royaltyFeeBp: NEW_ROYALTY_FEE}); + address[2] memory expectedRecipients; + + expectedRecipients[0] = takerUser; + expectedRecipients[1] = _royaltyRecipient; + + // Execute taker ask transaction + vm.prank(takerUser); + + vm.expectEmit(true, false, false, true); + + emit TakerAsk( + NonceInvalidationParameters({ + orderHash: _computeOrderHash(makerBid), + orderNonce: makerBid.orderNonce, + isNonceInvalidated: true + }), + takerUser, + makerUser, + makerBid.strategyId, + makerBid.currency, + makerBid.collection, + makerBid.itemIds, + makerBid.amounts, + expectedRecipients, + expectedFees + ); + + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulExecutionThroughWETH(makerUser, takerUser, price, expectedFees); + // Verify the nonce is marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, makerBid.orderNonce), MAGIC_VALUE_ORDER_NONCE_EXECUTED); + } + + /** + * One Hypercert is sold through a taker ask using WETH. Address zero is specified as the recipient in the taker + * struct. + */ + function testTakerAskHypercertWithAddressZeroSpecifiedAsRecipient(uint256 price) public { + vm.assume(price <= 2 ether); + _setUpUsers(); + + (OrderStructs.Maker memory makerBid, OrderStructs.Taker memory takerAsk) = + _createMockMakerBidAndTakerAsk(address(mockHypercertMinter), address(weth), true); + makerBid.price = price; + + bytes memory signature = _signMakerOrder(makerBid, makerUserPK); + + // Verify maker bid order + _assertValidMakerOrder(makerBid, signature); + + // Adjustment + takerAsk.recipient = address(0); + + // Mint asset + vm.prank(takerUser); + mockHypercertMinter.mintClaim(takerUser, 10_000, "https://example.com/1", FROM_CREATOR_ONLY); + + // Arrays for events + uint256[3] memory expectedFees = _calculateExpectedFees({price: price, royaltyFeeBp: 0}); + address[2] memory expectedRecipients; + + expectedRecipients[0] = takerUser; + expectedRecipients[1] = address(0); // No royalties + + // Execute taker ask transaction + vm.prank(takerUser); + vm.expectEmit(true, false, false, true); + + emit TakerAsk( + NonceInvalidationParameters({ + orderHash: _computeOrderHash(makerBid), + orderNonce: makerBid.orderNonce, + isNonceInvalidated: true + }), + takerUser, + makerUser, + makerBid.strategyId, + makerBid.currency, + makerBid.collection, + makerBid.itemIds, + makerBid.amounts, + expectedRecipients, + expectedFees + ); + + looksRareProtocol.executeTakerAsk(takerAsk, makerBid, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulExecutionThroughWETH(makerUser, takerUser, price, expectedFees); + } + + /** + * Three Hypercert fractions are sold through 3 taker bids in one transaction with non-atomicity. + */ + function testThreeTakerBidsHypercerts() public { + uint256 price = 0.015 ether; + _setUpUsers(); + + uint256 numberPurchases = 3; + + OrderStructs.Maker[] memory makerAsks = new OrderStructs.Maker[](numberPurchases); + OrderStructs.Taker[] memory takerBids = new OrderStructs.Taker[](numberPurchases); + bytes[] memory signatures = new bytes[](numberPurchases); + + for (uint256 i; i < numberPurchases; i++) { + // Mint asset + vm.prank(makerUser); + mockHypercertMinter.mintClaimWithFractions( + makerUser, 10_000, fractionUnits, "https://example.com/1", FROM_CREATOR_ONLY + ); + + makerAsks[i] = _createSingleItemMakerOrder({ + quoteType: QuoteType.Ask, + globalNonce: 0, + subsetNonce: 0, + strategyId: STANDARD_SALE_FOR_FIXED_PRICE_STRATEGY, + collectionType: CollectionType.Hypercert, + orderNonce: i, + collection: address(mockHypercertMinter), + currency: ETH, + signer: makerUser, + price: price, // Fixed + itemId: (1 << 128) + 1 + i + }); + + // Sign order + signatures[i] = _signMakerOrder(makerAsks[i], makerUserPK); + + takerBids[i] = _genericTakerOrder(); + } + + // Other execution parameters + OrderStructs.MerkleTree[] memory merkleTrees = new OrderStructs.MerkleTree[](numberPurchases); + + // Execute taker bid transaction + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + + for (uint256 i; i < numberPurchases; i++) { + // Taker user has received the asset + assertEq(mockHypercertMinter.ownerOf((1 << 128) + 1 + i), takerUser); + // Verify the nonce is marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, i), MAGIC_VALUE_ORDER_NONCE_EXECUTED); + } + + // Taker bid user pays the whole price + assertEq(address(takerUser).balance, _initialETHBalanceUser - (numberPurchases * price)); + // Maker ask user receives 99.5% of the whole price (0.5% protocol) + assertEq( + address(makerUser).balance, + _initialETHBalanceUser + + ((price * _sellerProceedBpWithStandardProtocolFeeBp) * numberPurchases) / ONE_HUNDRED_PERCENT_IN_BP + ); + // No leftover in the balance of the contract + assertEq(address(looksRareProtocol).balance, 0); + } + + /** + * Transaction cannot go through if atomic, goes through if non-atomic (fund returns to buyer). + */ + function testThreeTakerBidsHypercertsOneFails() public { + _setUpUsers(); + + uint256 numberPurchases = 3; + uint256 faultyPurchase = numberPurchases - 1; + uint256 faultyTokenId = (numberPurchases << 128) + 1; + + OrderStructs.Maker[] memory makerAsks = new OrderStructs.Maker[](numberPurchases); + OrderStructs.Taker[] memory takerBids = new OrderStructs.Taker[](numberPurchases); + bytes[] memory signatures = new bytes[](numberPurchases); + + for (uint256 i; i < numberPurchases; i++) { + // Mint asset + vm.prank(makerUser); + mockHypercertMinter.mintClaim(makerUser, 10_000, "https://example.com/1", FROM_CREATOR_ONLY); + + makerAsks[i] = _createSingleItemMakerOrder({ + quoteType: QuoteType.Ask, + globalNonce: 0, + subsetNonce: 0, + strategyId: STANDARD_SALE_FOR_FIXED_PRICE_STRATEGY, + collectionType: CollectionType.Hypercert, + orderNonce: i, + collection: address(mockHypercertMinter), + currency: ETH, + signer: makerUser, + price: 1.4 ether, // Fixed + itemId: ((i + 1) << 128) + 1 // First fraction of the i-th minted asset + }); + + // Sign order + signatures[i] = _signMakerOrder(makerAsks[i], makerUserPK); + + takerBids[i] = _genericTakerOrder(); + } + + // Transfer tokenId = 2 to random user + address randomUser = address(55); + vm.prank(makerUser); + mockHypercertMinter.safeTransferFrom(makerUser, randomUser, faultyTokenId, 1, ""); + + /** + * 1. The whole purchase fails if execution is atomic + */ + { + // Other execution parameters + OrderStructs.MerkleTree[] memory merkleTrees = new OrderStructs.MerkleTree[](numberPurchases); + + vm.expectRevert(ERC1155SafeTransferFromFail.selector); + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: 1.4 ether * numberPurchases}( + takerBids, makerAsks, signatures, merkleTrees, true + ); + } + + /** + * 2. The whole purchase doesn't fail if execution is not-atomic + */ + { + // Other execution parameters + OrderStructs.MerkleTree[] memory merkleTrees = new OrderStructs.MerkleTree[](numberPurchases); + + vm.prank(takerUser); + // Execute taker bid transaction + looksRareProtocol.executeMultipleTakerBids{value: 1.4 ether * numberPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + } + + for (uint256 i; i < faultyPurchase; i++) { + // Taker user has received the first two assets + assertEq(mockHypercertMinter.ownerOf(((i + 1) << 128) + 1), takerUser); + // Verify the first two nonces are marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, i), MAGIC_VALUE_ORDER_NONCE_EXECUTED); + } + + // Taker user has not received the asset + assertEq(mockHypercertMinter.ownerOf(faultyTokenId), randomUser); + // Verify the nonce is NOT marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, faultyTokenId), bytes32(0)); + // Taker bid user pays the whole price + assertEq(address(takerUser).balance, _initialETHBalanceUser - 1 - ((numberPurchases - 1) * 1.4 ether)); + // Maker ask user receives 99.5% of the whole price (0.5% protocol) + assertEq( + address(makerUser).balance, + _initialETHBalanceUser + + ((1.4 ether * _sellerProceedBpWithStandardProtocolFeeBp) * (numberPurchases - 1)) + / ONE_HUNDRED_PERCENT_IN_BP + ); + // 1 wei left in the balance of the contract + assertEq(address(looksRareProtocol).balance, 1); + } + + function testThreeTakerBidsHypercertsLengthsInvalid() public { + _setUpUsers(); + + uint256 price = 1.12121111111 ether; + uint256 numberPurchases = 3; + + OrderStructs.Taker[] memory takerBids = new OrderStructs.Taker[](numberPurchases); + bytes[] memory signatures = new bytes[](numberPurchases); + OrderStructs.MerkleTree[] memory merkleTrees = new OrderStructs.MerkleTree[](numberPurchases); + + // 1. Invalid maker asks length + OrderStructs.Maker[] memory makerAsks = new OrderStructs.Maker[](numberPurchases - 1); + + vm.expectRevert(LengthsInvalid.selector); + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + + // 2. Invalid signatures length + makerAsks = new OrderStructs.Maker[](numberPurchases); + signatures = new bytes[](numberPurchases - 1); + + vm.expectRevert(LengthsInvalid.selector); + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + + // 3. Invalid merkle trees length + signatures = new bytes[](numberPurchases); + merkleTrees = new OrderStructs.MerkleTree[](numberPurchases - 1); + + vm.expectRevert(LengthsInvalid.selector); + vm.prank(takerUser); + looksRareProtocol.executeMultipleTakerBids{value: price * numberPurchases}( + takerBids, makerAsks, signatures, merkleTrees, false + ); + } + + function _calculateExpectedFees(uint256 price, uint256 royaltyFeeBp) + private + pure + returns (uint256[3] memory expectedFees) + { + expectedFees[2] = (price * _standardProtocolFeeBp) / ONE_HUNDRED_PERCENT_IN_BP; + expectedFees[1] = (price * royaltyFeeBp) / ONE_HUNDRED_PERCENT_IN_BP; + if (expectedFees[2] + expectedFees[1] < ((price * _minTotalFeeBp) / ONE_HUNDRED_PERCENT_IN_BP)) { + expectedFees[2] = ((price * _minTotalFeeBp) / ONE_HUNDRED_PERCENT_IN_BP) - expectedFees[1]; + } + expectedFees[0] = price - (expectedFees[1] + expectedFees[2]); + } + + function _assertSuccessfulExecutionThroughWETH( + address buyer, + address seller, + uint256 price, + uint256[3] memory expectedFees + ) private { + // Buyer has received the asset + assertEq(mockHypercertMinter.ownerOf(fractionId), buyer); + // Buyer pays the whole price + assertEq(weth.balanceOf(buyer), _initialWETHBalanceUser - price); + // Seller receives 99.5% of the whole price + assertEq(weth.balanceOf(seller), _initialWETHBalanceUser + expectedFees[0]); + assertEq( + weth.balanceOf(address(protocolFeeRecipient)), + expectedFees[2], + "ProtocolFeeRecipient should receive 1.5% of the whole price" + ); + // Royalty recipient receives 0.5% of the whole price + assertEq(weth.balanceOf(_royaltyRecipient), _initialWETHBalanceRoyaltyRecipient + expectedFees[1]); + } + + function _assertSuccessfulExecutionThroughETH( + address buyer, + address seller, + uint256 price, + uint256[3] memory expectedFees + ) private { + assertEq(mockHypercertMinter.ownerOf(fractionId), buyer); + // Buyer pays the whole price + assertEq(address(buyer).balance, _initialETHBalanceUser - price); + // Seller receives 99.5% of the whole price + assertEq(address(seller).balance, _initialETHBalanceUser + expectedFees[0]); + // Royalty recipient receives 0.5% of the whole price + assertEq(address(_royaltyRecipient).balance, _initialETHBalanceRoyaltyRecipient + expectedFees[1]); + } +} diff --git a/contracts/test/foundry/marketplace/StrategyManager.t.sol b/contracts/test/foundry/marketplace/StrategyManager.t.sol index e59128f2..2962afb1 100644 --- a/contracts/test/foundry/marketplace/StrategyManager.t.sol +++ b/contracts/test/foundry/marketplace/StrategyManager.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; // LooksRare unopinionated libraries import {IOwnableTwoSteps} from "@looksrare/contracts-libs/contracts/interfaces/IOwnableTwoSteps.sol"; diff --git a/contracts/test/foundry/marketplace/TransferManager.t.sol b/contracts/test/foundry/marketplace/TransferManager.t.sol index d223cc59..e9e56855 100644 --- a/contracts/test/foundry/marketplace/TransferManager.t.sol +++ b/contracts/test/foundry/marketplace/TransferManager.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; // LooksRare unopinionated libraries import {IOwnableTwoSteps} from "@looksrare/contracts-libs/contracts/interfaces/IOwnableTwoSteps.sol"; @@ -11,10 +11,13 @@ import {OrderStructs} from "@hypercerts/marketplace/libraries/OrderStructs.sol"; import {LooksRareProtocol} from "@hypercerts/marketplace/LooksRareProtocol.sol"; import {ITransferManager, TransferManager} from "@hypercerts/marketplace/TransferManager.sol"; import {AmountInvalid, LengthsInvalid} from "@hypercerts/marketplace/errors/SharedErrors.sol"; +import {HypercertMinter} from "@hypercerts/protocol/HypercertMinter.sol"; +import {IHypercertToken} from "@hypercerts/protocol/interfaces/IHypercertToken.sol"; // Mocks and other utils import {MockERC721} from "../../mock/MockERC721.sol"; import {MockERC1155} from "../../mock/MockERC1155.sol"; +import {MockHypercertMinterUUPS} from "../../mock/MockHypercertMinterUUPS.sol"; import {TestHelpers} from "./utils/TestHelpers.sol"; import {TestParameters} from "./utils/TestParameters.sol"; @@ -25,7 +28,10 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { address[] public operators; MockERC721 public mockERC721; MockERC1155 public mockERC1155; + MockHypercertMinterUUPS public mockHypercertMinterUUPS; + TransferManager public transferManager; + HypercertMinter public mockHypercertMinter; uint256 private constant tokenIdERC721 = 55; uint256 private constant tokenId1ERC1155 = 1; @@ -33,6 +39,20 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { uint256 private constant tokenId2ERC1155 = 2; uint256 private constant amount2ERC1155 = 5; + // Set the hypercert claim and first fraction id + uint256 private constant claimId1Hypercert = 1 << 128; + uint256 private constant amount1Hypercert = 1; + uint256 private constant units1Hypercert = 10_000; + uint256 private constant fractionId1Hypercert = claimId1Hypercert + 1; + + uint256 private constant claimId2Hypercert = 2 << 128; + uint256 private constant amount2Hypercert = 1; + uint256 private constant units2Hypercert = 10_000; + uint256 private constant fractionId2Hypercert = claimId2Hypercert + 1; + + IHypercertToken.TransferRestrictions private constant FROM_CREATOR_ONLY = + IHypercertToken.TransferRestrictions.FromCreatorOnly; + /** * 0. Internal helper functions */ @@ -40,6 +60,8 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { function _grantApprovals(address user) private asPrankedUser(user) { mockERC721.setApprovalForAll(address(transferManager), true); mockERC1155.setApprovalForAll(address(transferManager), true); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); + address[] memory approvedOperators = new address[](1); approvedOperators[0] = _transferrer; @@ -59,6 +81,9 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager = new TransferManager(_owner); mockERC721 = new MockERC721(); mockERC1155 = new MockERC1155(); + mockHypercertMinterUUPS = new MockHypercertMinterUUPS(); + mockHypercertMinterUUPS.setUp(); + mockHypercertMinter = mockHypercertMinterUUPS.minter(); operators.push(_transferrer); vm.deal(_transferrer, 100 ether); @@ -109,6 +134,27 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { assertEq(mockERC1155.balanceOf(_recipient, itemId), amount); } + function testTransferSingleItemHypercert() public { + _allowOperator(_transferrer); + _grantApprovals(_sender); + + uint256 itemId = fractionId1Hypercert; + uint256 amount = amount1Hypercert; + + vm.prank(_sender); + mockHypercertMinter.mintClaim(_sender, units1Hypercert, "https://example.com/1", FROM_CREATOR_ONLY); + + uint256[] memory itemIds = new uint256[](1); + itemIds[0] = itemId; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.prank(_transferrer); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + + assertEq(mockHypercertMinter.balanceOf(_recipient, itemId), amount); + } + function testTransferBatchItemsERC721() public { _allowOperator(_transferrer); _grantApprovals(_sender); @@ -160,11 +206,40 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { assertEq(mockERC1155.balanceOf(_recipient, tokenId2), amount2); } - function testTransferBatchItemsAcrossCollectionERC721AndERC1155() public { + function testTransferBatchItemsHypercerts() public { _allowOperator(_transferrer); _grantApprovals(_sender); - ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItems(); + uint256 tokenId1 = fractionId1Hypercert; + uint256 amount1 = amount1Hypercert; + uint256 tokenId2 = fractionId2Hypercert; + uint256 amount2 = amount2Hypercert; + + vm.startPrank(_sender); + mockHypercertMinter.mintClaim(_sender, units1Hypercert, "https://example.com/1", FROM_CREATOR_ONLY); + mockHypercertMinter.mintClaim(_sender, units2Hypercert, "https://example.com/2", FROM_CREATOR_ONLY); + vm.stopPrank(); + + uint256[] memory itemIds = new uint256[](2); + itemIds[0] = tokenId1; + itemIds[1] = tokenId2; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount1; + amounts[1] = amount2; + + vm.prank(_transferrer); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + + assertEq(mockHypercertMinter.balanceOf(_recipient, tokenId1), amount1); + assertEq(mockHypercertMinter.balanceOf(_recipient, tokenId2), amount2); + } + + function testTransferBatchItemsAcrossCollectionERC721AndERC1155AndHypercert() public { + _allowOperator(_transferrer); + _grantApprovals(_sender); + + ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItemsPranked(_sender); vm.prank(_transferrer); transferManager.transferBatchItemsAcrossCollections(items, _sender, _recipient); @@ -172,11 +247,17 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { assertEq(mockERC721.ownerOf(tokenIdERC721), _recipient); assertEq(mockERC1155.balanceOf(_recipient, tokenId1ERC1155), amount1ERC1155); assertEq(mockERC1155.balanceOf(_recipient, tokenId2ERC1155), amount2ERC1155); + assertEq(mockHypercertMinter.balanceOf(_recipient, fractionId1Hypercert), amount1Hypercert); + assertEq(mockHypercertMinter.balanceOf(_recipient, fractionId2Hypercert), amount2Hypercert); } - function testTransferBatchItemsAcrossCollectionERC721AndERC1155ByOwner() public asPrankedUser(_sender) { + function testTransferBatchItemsAcrossCollectionERC721AndERC1155AndHypercertByOwner() + public + asPrankedUser(_sender) + { mockERC721.setApprovalForAll(address(transferManager), true); mockERC1155.setApprovalForAll(address(transferManager), true); + mockHypercertMinter.setApprovalForAll(address(transferManager), true); ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItems(); @@ -185,6 +266,8 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { assertEq(mockERC721.ownerOf(tokenIdERC721), _recipient); assertEq(mockERC1155.balanceOf(_recipient, tokenId1ERC1155), amount1ERC1155); assertEq(mockERC1155.balanceOf(_recipient, tokenId2ERC1155), amount2ERC1155); + assertEq(mockHypercertMinter.balanceOf(_recipient, fractionId1Hypercert), amount1Hypercert); + assertEq(mockHypercertMinter.balanceOf(_recipient, fractionId2Hypercert), amount2Hypercert); } /** @@ -228,6 +311,25 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferItemsERC1155(address(mockERC1155), _sender, _recipient, itemIds, amounts); } + function testTransferSingleItemHypercertAmountIsZero() public { + _allowOperator(_transferrer); + _grantApprovals(_sender); + + uint256 itemId = fractionId1Hypercert; + + vm.prank(_sender); + mockHypercertMinter.mintClaim(_sender, 1, "https://example.com/1", FROM_CREATOR_ONLY); + + uint256[] memory itemIds = new uint256[](1); + itemIds[0] = itemId; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 0; + + vm.expectRevert(AmountInvalid.selector); + vm.prank(_transferrer); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + } + function testTransferMultipleItemsERC1155AmountIsZero() public { _allowOperator(_transferrer); _grantApprovals(_sender); @@ -250,6 +352,30 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferItemsERC1155(address(mockERC1155), _sender, _recipient, itemIds, amounts); } + function testTransferMultipleItemsHypercertAmountIsZero() public { + _allowOperator(_transferrer); + _grantApprovals(_sender); + + uint256 itemIdOne = fractionId1Hypercert; + uint256 itemIdTwo = fractionId2Hypercert; + + vm.prank(_sender); + mockHypercertMinter.mintClaim(_sender, 1, "https://example.com/1", FROM_CREATOR_ONLY); + vm.prank(_sender); + mockHypercertMinter.mintClaim(_sender, 1, "https://example.com/2", FROM_CREATOR_ONLY); + + uint256[] memory itemIds = new uint256[](2); + itemIds[0] = itemIdOne; + itemIds[1] = itemIdTwo; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 0; + amounts[1] = 0; + + vm.expectRevert(AmountInvalid.selector); + vm.prank(_transferrer); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + } + function testTransferBatchItemsAcrossCollectionZeroLength() public { _allowOperator(_transferrer); _grantApprovals(_sender); @@ -267,7 +393,7 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { _allowOperator(_transferrer); _grantApprovals(_sender); - ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItems(); + ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItemsPranked(_sender); items[1].amounts[0] = amount; vm.expectRevert(AmountInvalid.selector); @@ -279,7 +405,7 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { _allowOperator(_transferrer); _grantApprovals(_sender); - ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItems(); + ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItemsPranked(_sender); items[0].amounts[0] = 0; vm.expectRevert(AmountInvalid.selector); @@ -287,11 +413,23 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferBatchItemsAcrossCollections(items, _sender, _recipient); } + function testCannotBatchTransferIfHypercertAmountIsZero() public { + _allowOperator(_transferrer); + _grantApprovals(_sender); + + ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItemsPranked(_sender); + items[2].amounts[0] = 0; + + vm.expectRevert(AmountInvalid.selector); + vm.prank(_sender); + transferManager.transferBatchItemsAcrossCollections(items, _sender, _recipient); + } + function testTransferBatchItemsAcrossCollectionPerCollectionItemIdsLengthZero() public { _allowOperator(_transferrer); _grantApprovals(_sender); - ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItems(); + ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItemsPranked(_sender); items[0].itemIds = new uint256[](0); items[0].amounts = new uint256[](0); @@ -364,6 +502,38 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferItemsERC1155(address(mockERC1155), _sender, _recipient, itemIds, amounts); } + function testCannotTransferHypercertIfOperatorApprovalsRevokedByUserOrOperatorRemovedByOwner() public { + _allowOperator(_transferrer); + _grantApprovals(_sender); + + // 1. User revokes the operator + vm.prank(_sender); + vm.expectEmit(false, false, false, true); + emit ApprovalsRemoved(_sender, operators); + transferManager.revokeApprovals(operators); + + uint256 itemId = fractionId1Hypercert; + uint256[] memory itemIds = new uint256[](1); + itemIds[0] = itemId; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 5; + + vm.prank(_transferrer); + vm.expectRevert(ITransferManager.TransferCallerInvalid.selector); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + + // 2. Sender grants again approvals but owner removes the operators + _grantApprovals(_sender); + vm.prank(_owner); + vm.expectEmit(false, false, false, true); + emit OperatorRemoved(_transferrer); + transferManager.removeOperator(_transferrer); + + vm.prank(_transferrer); + vm.expectRevert(ITransferManager.TransferCallerInvalid.selector); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + } + function testCannotBatchTransferIfOperatorApprovalsRevoked() public { _allowOperator(_transferrer); _grantApprovals(_sender); @@ -374,7 +544,7 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { emit ApprovalsRemoved(_sender, operators); transferManager.revokeApprovals(operators); - ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItems(); + ITransferManager.BatchTransferItem[] memory items = _generateValidBatchTransferItemsPranked(_sender); vm.prank(_transferrer); vm.expectRevert(ITransferManager.TransferCallerInvalid.selector); @@ -392,7 +562,7 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferBatchItemsAcrossCollections(items, _sender, _recipient); } - function testCannotTransferERC721OrERC1155IfArrayLengthIs0() public { + function testCannotTransferERC721OrERC1155orHypercertIfArrayLengthIs0() public { uint256[] memory emptyArrayUint256 = new uint256[](0); // 1. ERC721 @@ -406,6 +576,12 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferItemsERC1155( address(mockERC1155), _sender, _recipient, emptyArrayUint256, emptyArrayUint256 ); + + // 3. Hypercert length is 0 + vm.expectRevert(LengthsInvalid.selector); + transferManager.transferItemsHypercert( + address(mockHypercertMinter), _sender, _recipient, emptyArrayUint256, emptyArrayUint256 + ); } function testCannotTransferERC1155IfArrayLengthDiffers() public { @@ -416,6 +592,14 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { transferManager.transferItemsERC1155(address(mockERC1155), _sender, _recipient, itemIds, amounts); } + function testCannotTransferHypercertsIfArrayLengthDiffers() public { + uint256[] memory itemIds = new uint256[](2); + uint256[] memory amounts = new uint256[](3); + + vm.expectRevert(LengthsInvalid.selector); + transferManager.transferItemsHypercert(address(mockHypercertMinter), _sender, _recipient, itemIds, amounts); + } + function testUserCannotGrantOrRevokeApprovalsIfArrayLengthIs0() public { address[] memory emptyArrayAddresses = new address[](0); @@ -489,13 +673,83 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { } function _generateValidBatchTransferItems() private returns (BatchTransferItem[] memory items) { - items = new ITransferManager.BatchTransferItem[](2); + items = new ITransferManager.BatchTransferItem[](3); + + { + mockERC721.mint(_sender, tokenIdERC721); + mockERC1155.mint(_sender, tokenId1ERC1155, amount1ERC1155); + mockERC1155.mint(_sender, tokenId2ERC1155, amount2ERC1155); + + mockHypercertMinter.mintClaim(_sender, units1Hypercert, "https://example.com/1", FROM_CREATOR_ONLY); + mockHypercertMinter.mintClaim(_sender, units2Hypercert, "https://example.com/2", FROM_CREATOR_ONLY); + + uint256[] memory fractionIdsHypercerts = new uint256[](2); + fractionIdsHypercerts[0] = fractionId1Hypercert; + fractionIdsHypercerts[1] = fractionId2Hypercert; + + uint256[] memory amountsHypercerts = new uint256[](2); + amountsHypercerts[0] = amount1Hypercert; + amountsHypercerts[1] = amount2Hypercert; + + uint256[] memory tokenIdsERC1155 = new uint256[](2); + tokenIdsERC1155[0] = tokenId1ERC1155; + tokenIdsERC1155[1] = tokenId2ERC1155; + + uint256[] memory amountsERC1155 = new uint256[](2); + amountsERC1155[0] = amount1ERC1155; + amountsERC1155[1] = amount2ERC1155; + + uint256[] memory tokenIdsERC721 = new uint256[](1); + tokenIdsERC721[0] = tokenIdERC721; + + uint256[] memory amountsERC721 = new uint256[](1); + amountsERC721[0] = 1; + + items[0] = ITransferManager.BatchTransferItem({ + collection: address(mockERC1155), + collectionType: CollectionType.ERC1155, + itemIds: tokenIdsERC1155, + amounts: amountsERC1155 + }); + items[1] = ITransferManager.BatchTransferItem({ + collection: address(mockERC721), + collectionType: CollectionType.ERC721, + itemIds: tokenIdsERC721, + amounts: amountsERC721 + }); + items[2] = ITransferManager.BatchTransferItem({ + collection: address(mockHypercertMinter), + collectionType: CollectionType.Hypercert, + itemIds: fractionIdsHypercerts, + amounts: amountsHypercerts + }); + } + } + + function _generateValidBatchTransferItemsPranked(address pranker) + private + returns (BatchTransferItem[] memory items) + { + items = new ITransferManager.BatchTransferItem[](3); { mockERC721.mint(_sender, tokenIdERC721); mockERC1155.mint(_sender, tokenId1ERC1155, amount1ERC1155); mockERC1155.mint(_sender, tokenId2ERC1155, amount2ERC1155); + vm.startPrank(pranker); + mockHypercertMinter.mintClaim(pranker, units1Hypercert, "https://example.com/1", FROM_CREATOR_ONLY); + mockHypercertMinter.mintClaim(pranker, units2Hypercert, "https://example.com/2", FROM_CREATOR_ONLY); + vm.stopPrank(); + + uint256[] memory fractionIdsHypercerts = new uint256[](2); + fractionIdsHypercerts[0] = fractionId1Hypercert; + fractionIdsHypercerts[1] = fractionId2Hypercert; + + uint256[] memory amountsHypercerts = new uint256[](2); + amountsHypercerts[0] = amount1Hypercert; + amountsHypercerts[1] = amount2Hypercert; + uint256[] memory tokenIdsERC1155 = new uint256[](2); tokenIdsERC1155[0] = tokenId1ERC1155; tokenIdsERC1155[1] = tokenId2ERC1155; @@ -522,6 +776,12 @@ contract TransferManagerTest is ITransferManager, TestHelpers, TestParameters { itemIds: tokenIdsERC721, amounts: amountsERC721 }); + items[2] = ITransferManager.BatchTransferItem({ + collection: address(mockHypercertMinter), + collectionType: CollectionType.Hypercert, + itemIds: fractionIdsHypercerts, + amounts: amountsHypercerts + }); } } } diff --git a/contracts/test/foundry/marketplace/utils/MockOrderGenerator.sol b/contracts/test/foundry/marketplace/utils/MockOrderGenerator.sol index d6e695f5..f0cc0b9e 100644 --- a/contracts/test/foundry/marketplace/utils/MockOrderGenerator.sol +++ b/contracts/test/foundry/marketplace/utils/MockOrderGenerator.sol @@ -20,8 +20,16 @@ contract MockOrderGenerator is ProtocolHelpers { view returns (OrderStructs.Maker memory newMakerAsk, OrderStructs.Taker memory newTakerBid) { - CollectionType collectionType = _getCollectionType(collection); + return _createMockMakerAskAndTakerBid(collection, false); + } + function _createMockMakerAskAndTakerBid(address collection, bool hypercerts) + internal + view + returns (OrderStructs.Maker memory newMakerAsk, OrderStructs.Taker memory newTakerBid) + { + CollectionType collectionType = hypercerts ? CollectionType.Hypercert : _getCollectionType(collection); + uint256 itemId = hypercerts ? ((1 << 128) + 1) : 420; newMakerAsk = _createSingleItemMakerOrder({ quoteType: QuoteType.Ask, globalNonce: 0, @@ -33,7 +41,7 @@ contract MockOrderGenerator is ProtocolHelpers { currency: ETH, signer: makerUser, price: 1 ether, - itemId: 420 + itemId: itemId }); newTakerBid = OrderStructs.Taker(takerUser, abi.encode()); @@ -44,7 +52,15 @@ contract MockOrderGenerator is ProtocolHelpers { view returns (OrderStructs.Maker memory newMakerBid, OrderStructs.Taker memory newTakerAsk) { - CollectionType collectionType = _getCollectionType(collection); + return _createMockMakerBidAndTakerAsk(collection, currency, false); + } + + function _createMockMakerBidAndTakerAsk(address collection, address currency, bool hypercerts) + internal + view + returns (OrderStructs.Maker memory newMakerBid, OrderStructs.Taker memory newTakerAsk) + { + CollectionType collectionType = hypercerts ? CollectionType.Hypercert : _getCollectionType(collection); newMakerBid = _createSingleItemMakerOrder({ quoteType: QuoteType.Bid, @@ -57,7 +73,7 @@ contract MockOrderGenerator is ProtocolHelpers { currency: currency, signer: makerUser, price: 1 ether, - itemId: 420 + itemId: hypercerts ? (1 << 128) + 1 : 420 }); newTakerAsk = OrderStructs.Taker(takerUser, abi.encode()); @@ -68,7 +84,15 @@ contract MockOrderGenerator is ProtocolHelpers { view returns (OrderStructs.Maker memory newMakerAsk, OrderStructs.Taker memory newTakerBid) { - CollectionType collectionType = _getCollectionType(collection); + return _createMockMakerAskAndTakerBidWithBundle(collection, numberTokens, false); + } + + function _createMockMakerAskAndTakerBidWithBundle(address collection, uint256 numberTokens, bool hypercerts) + internal + view + returns (OrderStructs.Maker memory newMakerAsk, OrderStructs.Taker memory newTakerBid) + { + CollectionType collectionType = hypercerts ? CollectionType.Hypercert : _getCollectionType(collection); (uint256[] memory itemIds, uint256[] memory amounts) = _setBundleItemIdsAndAmounts(collectionType, numberTokens); @@ -95,7 +119,16 @@ contract MockOrderGenerator is ProtocolHelpers { view returns (OrderStructs.Maker memory newMakerBid, OrderStructs.Taker memory newTakerAsk) { - CollectionType collectionType = _getCollectionType(collection); + return _createMockMakerBidAndTakerAskWithBundle(collection, currency, numberTokens, false); + } + + function _createMockMakerBidAndTakerAskWithBundle( + address collection, + address currency, + uint256 numberTokens, + bool hypercerts + ) internal view returns (OrderStructs.Maker memory newMakerBid, OrderStructs.Taker memory newTakerAsk) { + CollectionType collectionType = hypercerts ? CollectionType.Hypercert : _getCollectionType(collection); (uint256[] memory itemIds, uint256[] memory amounts) = _setBundleItemIdsAndAmounts(collectionType, numberTokens); @@ -138,6 +171,9 @@ contract MockOrderGenerator is ProtocolHelpers { itemIds[i] = i; if (collectionType != CollectionType.ERC1155) { amounts[i] = 1; + } else if (collectionType == CollectionType.Hypercert) { + itemIds[i] = (1 << 128) + 1 + i; + amounts[i] = 1; } else { amounts[i] = 1 + i; } diff --git a/contracts/test/mock/MockHypercertMinterUUPS.sol b/contracts/test/mock/MockHypercertMinterUUPS.sol new file mode 100644 index 00000000..e883600e --- /dev/null +++ b/contracts/test/mock/MockHypercertMinterUUPS.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {HypercertMinter} from "@hypercerts/protocol/HypercertMinter.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract UUPSProxy is ERC1967Proxy { + constructor(address implementation, bytes memory data) ERC1967Proxy(implementation, data) {} +} + +contract MockHypercertMinterUUPS { + HypercertMinter public instance; + UUPSProxy public proxy; + HypercertMinter public minter; + + function setUp() public { + instance = new HypercertMinter(); + proxy = new UUPSProxy(address(instance), ""); + minter = HypercertMinter(address(proxy)); + } +}