diff --git a/contracts/src/marketplace/LooksRareProtocol.sol b/contracts/src/marketplace/LooksRareProtocol.sol index 2a40f64e..3eb51f1b 100644 --- a/contracts/src/marketplace/LooksRareProtocol.sol +++ b/contracts/src/marketplace/LooksRareProtocol.sol @@ -31,13 +31,13 @@ import { // Direct dependencies import {TransferSelectorNFT} from "./TransferSelectorNFT.sol"; import {BatchOrderTypehashRegistry} from "./BatchOrderTypehashRegistry.sol"; -import {StrategyHypercertFractionOffer} from "./executionStrategies/StrategyHypercertFractionOffer.sol"; // Constants import {MAX_CALLDATA_PROOF_LENGTH, ONE_HUNDRED_PERCENT_IN_BP} from "./constants/NumericConstants.sol"; // Enums import {QuoteType} from "./enums/QuoteType.sol"; +import {CollectionType} from "./enums/CollectionType.sol"; /** * @title LooksRareProtocol @@ -371,7 +371,7 @@ contract LooksRareProtocol is _updateUserOrderNonce(isNonceInvalidated, signer, makerBid.orderNonce, orderHash); // Taker action goes first - _transferNFT(makerBid.collection, makerBid.collectionType, msg.sender, signer, itemIds, amounts); + _executeTakerAskTakerAction(makerBid, takerAsk, msg.sender, signer, itemIds, amounts); // Maker action goes second _transferToAskRecipientAndCreatorIfAny(recipients, feeAmounts, makerBid.currency, signer); @@ -467,6 +467,23 @@ contract LooksRareProtocol is return feeAmounts[2]; } + function _executeTakerAskTakerAction( + OrderStructs.Maker calldata makerBid, + OrderStructs.Taker calldata takerAsk, + address sender, + address recipient, + uint256[] memory itemIds, + uint256[] memory amounts + ) internal { + if (makerBid.collectionType == CollectionType.Hypercert) { + _transferHypercertFraction( + makerBid.collection, makerBid.collectionType, makerBid.strategyId, sender, recipient, itemIds, amounts + ); + } else { + _transferNFT(makerBid.collection, makerBid.collectionType, sender, recipient, itemIds, amounts); + } + } + function _executeTakerBidMakerAction( OrderStructs.Maker calldata makerAsk, OrderStructs.Taker calldata takerBid, @@ -475,10 +492,11 @@ contract LooksRareProtocol is uint256[] memory itemIds, uint256[] memory amounts ) internal { - if (makerAsk.collectionType == 3) { + if (makerAsk.collectionType == CollectionType.Hypercert) { _transferHypercertFraction( makerAsk.collection, makerAsk.collectionType, + makerAsk.strategyId, sender, takerBid.recipient == address(0) ? recipient : takerBid.recipient, itemIds, diff --git a/contracts/src/marketplace/TransferManager.sol b/contracts/src/marketplace/TransferManager.sol index 4b885038..6ace6f93 100644 --- a/contracts/src/marketplace/TransferManager.sol +++ b/contracts/src/marketplace/TransferManager.sol @@ -31,7 +31,7 @@ import {CollectionType} from "./enums/CollectionType.sol"; * Collection type "3" refers to Hyperboard transfer functions. * @dev "Safe" transfer functions for ERC721 are not implemented since they come with added gas costs * to verify if the recipient is a contract as it requires verifying the receiver interface is valid. - * @author LooksRare protocol team (👀,💎) + * @author LooksRare protocol team (👀,💎); bitbeckers */ contract TransferManager is ITransferManager, @@ -177,13 +177,13 @@ contract TransferManager is } /** - * @notice This function transfers items for a single Hypercert. + * @notice This function splits and transfers a fraction of a hypercert. * @param collection Collection address * @param from Sender address * @param to Recipient address * @param itemIds Array of itemIds * @param amounts Array of amounts - * @dev It does not allow batch transferring if from = msg.sender since native function should be used. + * @dev It does not allow batch transferring. */ function splitItemsHypercert( address collection, @@ -192,8 +192,6 @@ contract TransferManager is uint256[] calldata itemIds, uint256[] calldata amounts ) external { - IHypercertToken hypercert = IHypercertToken(collection); - if (itemIds.length != 1 || amounts.length != 1) { revert LengthsInvalid(); } @@ -204,14 +202,19 @@ contract TransferManager is revert AmountInvalid(); } - if (IHypercertToken(collection).unitsOf(itemIds[0]) <= amounts[0]) { - revert UnitAmountInvalid(); - } - uint256[] memory newAmounts = new uint256[](2); - newAmounts[0] = hypercert.unitsOf(itemIds[0]) - amounts[0]; + + //The new amount is the difference between the total amount and the amount being split. + //This will underflow if the amount being split is greater than the total amount. + newAmounts[0] = IHypercertToken(collection).unitsOf(itemIds[0]) - amounts[0]; newAmounts[1] = amounts[0]; - _executeHypercertSplitFraction(collection, from, to, itemIds[0], newAmounts); + + // If the new amount is 0, then the split is will revert but the whole fraction can be transferred. + if (newAmounts[0] == 0) { + _executeERC1155SafeTransferFrom(collection, from, to, itemIds[0], 1); + } else { + _executeHypercertSplitFraction(collection, to, itemIds[0], newAmounts); + } } /** diff --git a/contracts/src/marketplace/TransferSelectorNFT.sol b/contracts/src/marketplace/TransferSelectorNFT.sol index 610a1b6d..347dae97 100644 --- a/contracts/src/marketplace/TransferSelectorNFT.sol +++ b/contracts/src/marketplace/TransferSelectorNFT.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.17; import {PackableReentrancyGuard} from "@looksrare/contracts-libs/contracts/PackableReentrancyGuard.sol"; import {ExecutionManager} from "./ExecutionManager.sol"; import {TransferManager} from "./TransferManager.sol"; +import {StrategyHypercertFractionOffer} from "./executionStrategies/StrategyHypercertFractionOffer.sol"; // Libraries import {OrderStructs} from "./libraries/OrderStructs.sol"; @@ -23,7 +24,7 @@ import {IHypercertToken} from "../protocol/interfaces/IHypercertToken.sol"; contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard { error UnsupportedCollectionType(); /** - * @notice Transfer manager for ERC721 and ERC1155. + * @notice Transfer manager for ERC721, ERC1155 and Hypercerts. */ TransferManager public immutable transferManager; @@ -61,8 +62,6 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard { transferManager.transferItemsERC721(collection, sender, recipient, itemIds, amounts); } else if (collectionType == CollectionType.ERC1155) { transferManager.transferItemsERC1155(collection, sender, recipient, itemIds, amounts); - } else if (collectionType == CollectionType.Hypercert) { - transferManager.transferItemsHypercert(collection, sender, recipient, itemIds, amounts); } else { revert UnsupportedCollectionType(); } @@ -80,6 +79,7 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard { function _transferHypercertFraction( address collection, CollectionType collectionType, + uint256 strategyId, address sender, address recipient, uint256[] memory itemIds, @@ -90,13 +90,13 @@ contract TransferSelectorNFT is ExecutionManager, PackableReentrancyGuard { } if ( - strategyInfo[makerAsk.strategyId].selector + strategyInfo[strategyId].selector == StrategyHypercertFractionOffer.executeHypercertFractionStrategyWithTakerBid.selector - || strategyInfo[makerAsk.strategyId].selector + || strategyInfo[strategyId].selector == StrategyHypercertFractionOffer.executeHypercertFractionStrategyWithTakerBidWithAllowlist.selector ) { transferManager.splitItemsHypercert(collection, sender, recipient, itemIds, amounts); - } else if (amounts[0] == 1) { + } else { transferManager.transferItemsHypercert(collection, sender, recipient, itemIds, amounts); } } diff --git a/contracts/src/marketplace/errors/SharedErrors.sol b/contracts/src/marketplace/errors/SharedErrors.sol index bb123902..9ac87690 100644 --- a/contracts/src/marketplace/errors/SharedErrors.sol +++ b/contracts/src/marketplace/errors/SharedErrors.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.17; /** * @notice It is returned if the amount is invalid. - * For ERC721, any number that is not 1. For ERC1155, if amount is 0. + * For ERC721, any number that is not 1. For ERC1155 and Hypercert, if amount is 0. */ error AmountInvalid(); diff --git a/contracts/src/marketplace/executionStrategies/StrategyHypercertFractionOffer.sol b/contracts/src/marketplace/executionStrategies/StrategyHypercertFractionOffer.sol index eab9d6f9..98c1c498 100644 --- a/contracts/src/marketplace/executionStrategies/StrategyHypercertFractionOffer.sol +++ b/contracts/src/marketplace/executionStrategies/StrategyHypercertFractionOffer.sol @@ -14,7 +14,14 @@ import {MerkleProofMemory} from "../libraries/OpenZeppelin/MerkleProofMemory.sol import {QuoteType} from "../enums/QuoteType.sol"; // Shared errors -import {OrderInvalid, FunctionSelectorInvalid, MerkleProofInvalid, QuoteTypeInvalid} from "../errors/SharedErrors.sol"; +import { + AmountInvalid, + LengthsInvalid, + OrderInvalid, + FunctionSelectorInvalid, + MerkleProofInvalid, + QuoteTypeInvalid +} from "../errors/SharedErrors.sol"; // Base strategy contracts import {BaseStrategy, IStrategy} from "./BaseStrategy.sol"; @@ -58,35 +65,42 @@ contract StrategyHypercertFractionOffer is BaseStrategy { ) external view - returns (uint256 price, uint256[] memory itemIds, uint256[] calldata amounts, bool isNonceInvalidated) + returns (uint256 price, uint256[] memory itemIds, uint256[] memory amounts, bool isNonceInvalidated) { - amounts = makerAsk.amounts; itemIds = makerAsk.itemIds; // A collection order can only be executable for 1 itemId but the actual quantity to fill can vary - if (amounts.length != 1 || itemIds.length != 1) { - revert OrderInvalid(); + if (makerAsk.amounts.length != 1 || itemIds.length != 1) { + revert LengthsInvalid(); } - //units, amount, proof[] - (uint256 unitAmount, uint256 acceptedTokenAmount) = - abi.decode(takerBid.additionalParameters, (uint256, uint256)); + if (makerAsk.amounts[0] == 0) { + revert AmountInvalid(); + } + + //units, pricePerUnit + (uint256 unitAmount, uint256 pricePerUnit) = abi.decode(takerBid.additionalParameters, (uint256, uint256)); //minUnitAmount, maxUnitAmount, root (uint256 minUnitAmount, uint256 maxUnitAmount) = abi.decode(makerAsk.additionalParameters, (uint256, uint256)); // A collection order can only be executable for 1 itemId but quantity to fill can vary if ( - makerAsk.amounts.length != 1 || makerAsk.itemIds.length != 1 || minUnitAmount > maxUnitAmount - || unitAmount < minUnitAmount || makerAsk.price > acceptedTokenAmount || makerAsk.price == 0 - || IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) < amounts[0] + minUnitAmount > maxUnitAmount || unitAmount == 0 || unitAmount < minUnitAmount || unitAmount > maxUnitAmount + || pricePerUnit < makerAsk.price || makerAsk.price == 0 + || IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) < unitAmount ) { revert OrderInvalid(); } - price = acceptedTokenAmount * unitAmount; + uint256[] memory amountsToFill = new uint256[](1); + amountsToFill[0] = unitAmount; + amounts = amountsToFill; - isNonceInvalidated = true; + price = unitAmount * pricePerUnit; + // If the amount to fill is equal to the amount of units in the hypercert, we transfer the fraction. + // Otherwise, we do not invalidate the nonce because it is a partial fill. + isNonceInvalidated = IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) == unitAmount; } /** @@ -103,42 +117,46 @@ contract StrategyHypercertFractionOffer is BaseStrategy { OrderStructs.Maker calldata makerAsk ) external - pure + view returns (uint256 price, uint256[] memory itemIds, uint256[] memory amounts, bool isNonceInvalidated) { itemIds = makerAsk.itemIds; // A collection order can only be executable for 1 itemId but the actual quantity to fill can vary if (makerAsk.amounts.length != 1 || itemIds.length != 1) { - revert OrderInvalid(); + revert LengthsInvalid(); + } + + if (makerAsk.amounts[0] == 0) { + revert AmountInvalid(); } - //units, amount, proof[] + //units, pricePerUnit, proof[] (uint256 unitAmount, uint256 pricePerUnit, bytes32[] memory proof) = abi.decode(takerBid.additionalParameters, (uint256, uint256, bytes32[])); - // A bid needs to at least match the minimum price per unit - if (pricePerUnit < makerAsk.price) { - revert OrderInvalid(); - } - - price = unitAmount * pricePerUnit; - amounts = new uint256[](1); - amounts[0] = unitAmount; + //minUnitAmount, maxUnitAmount, root + (uint256 minUnitAmount, uint256 maxUnitAmount, bytes32 root) = + abi.decode(makerAsk.additionalParameters, (uint256, uint256, bytes32)); - // A collection order can only be executable for 1 itemId but the actual quantity to fill can vary - if (amounts.length != 1 || itemIds.length != 1) { + // A collection order can only be executable for 1 itemId but quantity to fill can vary + if ( + minUnitAmount > maxUnitAmount || unitAmount == 0 || unitAmount < minUnitAmount || unitAmount > maxUnitAmount + || pricePerUnit < makerAsk.price || makerAsk.price == 0 + || IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) < unitAmount + ) { revert OrderInvalid(); } - //minUnitAmount, maxUnitAmount, root - (uint256 minUnitAmount, uint256 maxUnitAmount, bytes32 root) = - abi.decode(makerAsk.additionalParameters, (uint256, uint256, bytes32)); + uint256[] memory amountsToFill = new uint256[](1); + amountsToFill[0] = unitAmount; + amounts = amountsToFill; + + price = unitAmount * pricePerUnit; - // Nonce is not invalidated because it can be a partial fill - // @dev This strategy represents a partial fill. The protocol will call transfer if the bid would clear the - // offered fraction. - isNonceInvalidated = false; + // If the amount to fill is equal to the amount of units in the hypercert, we transfer the fraction. + // Otherwise, we do not invalidate the nonce because it is a partial fill. + isNonceInvalidated = IHypercertToken(makerAsk.collection).unitsOf(itemIds[0]) == unitAmount; bytes32 node = keccak256(abi.encodePacked(takerBid.recipient)); diff --git a/contracts/src/marketplace/libraries/LowLevelHypercertCaller.sol b/contracts/src/marketplace/libraries/LowLevelHypercertCaller.sol index a48df879..cd79e160 100644 --- a/contracts/src/marketplace/libraries/LowLevelHypercertCaller.sol +++ b/contracts/src/marketplace/libraries/LowLevelHypercertCaller.sol @@ -16,19 +16,14 @@ contract LowLevelHypercertCaller { /** * @notice Execute Hypercert splitFraction * @param collection Address of the collection - * @param from Address of the sender * @param to Address of the recipient * @param tokenId tokenId to transfer * @param amounts split distribution */ - function _executeHypercertSplitFraction( - address collection, - address from, - address to, - uint256 tokenId, - uint256[] memory amounts - ) internal { + function _executeHypercertSplitFraction(address collection, address to, uint256 tokenId, uint256[] memory amounts) + internal + { if (collection.code.length == 0) { revert NotAContract(); } diff --git a/contracts/test/foundry/marketplace/executionStrategies/HypercertFractionOffers.t.sol b/contracts/test/foundry/marketplace/executionStrategies/HypercertFractionOffers.t.sol index f8bbf0ee..c04a128a 100644 --- a/contracts/test/foundry/marketplace/executionStrategies/HypercertFractionOffers.t.sol +++ b/contracts/test/foundry/marketplace/executionStrategies/HypercertFractionOffers.t.sol @@ -10,6 +10,7 @@ import {OrderStructs} from "@hypercerts/marketplace/libraries/OrderStructs.sol"; // Shared errors import { AmountInvalid, + LengthsInvalid, OrderInvalid, FunctionSelectorInvalid, MerkleProofInvalid, @@ -136,7 +137,7 @@ contract HypercertFractionOffersTest is ProtocolBase { _assertOrderIsInvalid(makerAsk, false); _assertMakerOrderReturnValidationCode(makerAsk, signature, MAKER_ORDER_PERMANENTLY_INVALID_NON_STANDARD_SALE); - vm.expectRevert(OrderInvalid.selector); + vm.expectRevert(LengthsInvalid.selector); looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); // // With proof @@ -146,7 +147,7 @@ contract HypercertFractionOffersTest is ProtocolBase { _assertOrderIsInvalid(makerAsk, true); _assertMakerOrderReturnValidationCode(makerAsk, signature, MAKER_ORDER_PERMANENTLY_INVALID_NON_STANDARD_SALE); - vm.expectRevert(OrderInvalid.selector); + vm.expectRevert(LengthsInvalid.selector); looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); } @@ -170,20 +171,178 @@ contract HypercertFractionOffersTest is ProtocolBase { looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); } + /** + * A collection offer without merkle tree criteria + */ + + function testTakerBidHypercertFractionOrderPartialFill() public { + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMakerAskAndTakerBidHypercert(true); + + // Sign order + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Verify validity of maker bid order + _assertOrderIsValid(makerAsk, false); + _assertValidMakerOrder(makerAsk, signature); + + // Execute taker ask transaction + vm.prank(takerUser); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulTakerBid(makerAsk, takerBid, (1 << 128) + 1); + } + + function testTakerBidHypercertFractionOrderFullFill() public { + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMakerAskAndTakerBidHypercert(true); + + makerAsk.additionalParameters = abi.encode(minUnitAmount, 10_000); + takerBid.additionalParameters = abi.encode(10_000, price); + + // Sign order + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Verify validity of maker bid order + _assertOrderIsValid(makerAsk, false); + _assertValidMakerOrder(makerAsk, signature); + + // Execute taker ask transaction + vm.prank(takerUser); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulTakerBidFullFraction(makerAsk, takerBid, (1 << 128) + 1); + } + + function testTakerBidHypercertFractionOrderPartialAndFullFill() public { + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMakerAskAndTakerBidHypercert(true); + + makerAsk.amounts[0] = 10_000; + makerAsk.additionalParameters = abi.encode(minUnitAmount, 10_000); + + takerBid.additionalParameters = abi.encode(3000, price); + + uint256 fractionId = (1 << 128) + 1; + + // Sign order + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Verify validity of maker bid order + _assertOrderIsValid(makerAsk, false); + _assertValidMakerOrder(makerAsk, signature); + + // Execute taker ask transaction partial fill; buy 3000 units + vm.prank(takerUser); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + _assertSuccessfulTakerBid(makerAsk, takerBid, fractionId); + + makerAsk.additionalParameters = abi.encode(minUnitAmount, 10_000); + takerBid.additionalParameters = abi.encode(7000, price); + + // Execute taker ask transaction full fill; buy remaining 7000 units + vm.prank(takerUser); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + + //units, amount, currency, proof[] + (uint256 unitAmount, uint256 bidPrice) = abi.decode(takerBid.additionalParameters, (uint256, uint256)); + + // Taker user has received the asset + assertEq(mockHypercertMinter.ownerOf(fractionId), takerUser); + + // Units have been transfered + assertEq(mockHypercertMinter.unitsOf(fractionId), unitAmount); + + // Verify the nonce is marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, makerAsk.orderNonce), MAGIC_VALUE_ORDER_NONCE_EXECUTED); + } + + function testTakerBidHypercertFractionOrderUnitsOutOfMaxRange() public { + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMakerAskAndTakerBidHypercert(true); + + makerAsk.additionalParameters = abi.encode(minUnitAmount, 100); + takerBid.additionalParameters = abi.encode(101, price); + + // Sign order + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Verify validity of maker bid order + _assertOrderIsValid(makerAsk, false); + _assertValidMakerOrder(makerAsk, signature); + + // Execute taker ask transaction + vm.prank(takerUser); + vm.expectRevert(OrderInvalid.selector); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + } + + function testTakerBidHypercertFractionOrderUnitsOutOfMinRange() public { + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMakerAskAndTakerBidHypercert(true); + + makerAsk.additionalParameters = abi.encode(5, 100); + takerBid.additionalParameters = abi.encode(2, price); + + // Sign order + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Verify validity of maker bid order + _assertOrderIsValid(makerAsk, false); + _assertValidMakerOrder(makerAsk, signature); + + // Execute taker ask transaction + vm.prank(takerUser); + vm.expectRevert(OrderInvalid.selector); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + } + + function testTakerBidHypercertFractionOrderBidPriceTooLow() public { + _setUpUsers(); + + (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = + _createMakerAskAndTakerBidHypercert(true); + + makerAsk.additionalParameters = abi.encode(minUnitAmount, maxUnitAmount); + takerBid.additionalParameters = abi.encode(maxUnitAmount, price - 1); + + // Sign order + bytes memory signature = _signMakerOrder(makerAsk, makerUserPK); + + // Verify validity of maker bid order + _assertOrderIsValid(makerAsk, false); + _assertValidMakerOrder(makerAsk, signature); + + // Execute taker ask transaction + vm.prank(takerUser); + vm.expectRevert(OrderInvalid.selector); + looksRareProtocol.executeTakerBid(takerBid, makerAsk, signature, _EMPTY_MERKLE_TREE); + } + /** * A collection offer with merkle tree criteria */ /** * TAKER ALLOWLIST */ - function testTakerBidCollectionOrderWithMerkleTreeHypercertAccountAllowlist() public { + function testTakerBidHypercertFractionOrderWithMerkleTreeHypercertAccountAllowlist() public { _setUpUsers(); (OrderStructs.Maker memory makerAsk, OrderStructs.Taker memory takerBid) = _createMakerAskAndTakerBidHypercert(false); address accountInMerkleTree = takerUser; - uint256 tokenIdInMerkleTree = 2; (bytes32 merkleRoot, bytes32[] memory proof) = _mintNFTsToOwnerAndGetMerkleRootAndProofAccountAllowlist({ owner: makerUser, numberOfAccountsInMerkleTree: 5, @@ -218,7 +377,6 @@ contract HypercertFractionOffersTest is ProtocolBase { _createMakerAskAndTakerBidHypercert(false); address accountInMerkleTree = takerUser; - uint256 tokenIdInMerkleTree = 2; (bytes32 merkleRoot, bytes32[] memory proof) = _mintNFTsToOwnerAndGetMerkleRootAndProofAccountAllowlist({ owner: makerUser, numberOfAccountsInMerkleTree: 5, @@ -266,7 +424,6 @@ contract HypercertFractionOffersTest is ProtocolBase { // 2. Amount is 0 (with merkle proof) makerAsk.strategyId = 2; address accountInMerkleTree = takerUser; - uint256 tokenIdInMerkleTree = 2; (bytes32 merkleRoot, bytes32[] memory proof) = _mintNFTsToOwnerAndGetMerkleRootAndProofAccountAllowlist({ owner: makerUser, numberOfAccountsInMerkleTree: 5, @@ -397,7 +554,7 @@ contract HypercertFractionOffersTest is ProtocolBase { uint256 fractionId ) private { //units, amount, currency, proof[] - (uint256 unitAmount, uint256 price) = abi.decode(takerBid.additionalParameters, (uint256, uint256)); + (uint256 unitAmount, uint256 bidPrice) = abi.decode(takerBid.additionalParameters, (uint256, uint256)); // Taker user has received the asset assertEq(mockHypercertMinter.ownerOf(fractionId), makerUser); @@ -408,14 +565,40 @@ contract HypercertFractionOffersTest is ProtocolBase { assertEq(mockHypercertMinter.unitsOf(fractionId + 1), unitAmount); // Maker bid user pays the whole price - assertEq(weth.balanceOf(takerUser), _initialWETHBalanceUser - (unitAmount * price)); + assertEq(weth.balanceOf(takerUser), _initialWETHBalanceUser - (unitAmount * bidPrice)); // Taker ask user receives 99.5% of the whole price (0.5% protocol) assertEq( weth.balanceOf(makerUser), _initialWETHBalanceUser - + (unitAmount * price * _sellerProceedBpWithStandardProtocolFeeBp) / ONE_HUNDRED_PERCENT_IN_BP + + (unitAmount * bidPrice * _sellerProceedBpWithStandardProtocolFeeBp) / ONE_HUNDRED_PERCENT_IN_BP ); // Verify the nonce is marked as executed assertEq(looksRareProtocol.userOrderNonce(makerUser, makerAsk.orderNonce), _computeOrderHash(makerAsk)); } + + function _assertSuccessfulTakerBidFullFraction( + OrderStructs.Maker memory makerAsk, + OrderStructs.Taker memory takerBid, + uint256 fractionId + ) private { + //units, amount, currency, proof[] + (uint256 unitAmount, uint256 bidPrice) = abi.decode(takerBid.additionalParameters, (uint256, uint256)); + + // Taker user has received the asset + assertEq(mockHypercertMinter.ownerOf(fractionId), takerUser); + + // Units have been transfered + assertEq(mockHypercertMinter.unitsOf(fractionId), unitAmount); + + // Maker bid user pays the whole price + assertEq(weth.balanceOf(takerUser), _initialWETHBalanceUser - (unitAmount * bidPrice)); + // Taker ask user receives 99.5% of the whole price (0.5% protocol) + assertEq( + weth.balanceOf(makerUser), + _initialWETHBalanceUser + + (unitAmount * bidPrice * _sellerProceedBpWithStandardProtocolFeeBp) / ONE_HUNDRED_PERCENT_IN_BP + ); + // Verify the nonce is marked as executed + assertEq(looksRareProtocol.userOrderNonce(makerUser, makerAsk.orderNonce), MAGIC_VALUE_ORDER_NONCE_EXECUTED); + } }