diff --git a/README.md b/README.md index cc22b25..91b4991 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ | lukso-testnet | LSP7Marketplace | 0x61c3dd3476a88de7a2bae7e2bc55889185faea1e | | lukso-testnet | LSP8Listings | 0x1dabeddbc94847b4ca9027073e545f67917a84f6 | | lukso-testnet | LSP8Offers | 0x84c0b26747a4f997ab1bfe5110a9579de2c0aeaf | -| lukso-testnet | LSP8Orders | 0xf5e502235f1b21413ba96e8afb92004b7ac5b1f2 | +| lukso-testnet | LSP8Orders | 0x3e81a952670a5df4062296d644dd5bf05cd475cb | | lukso-testnet | LSP8Auctions | 0xb20f814e55720e477640717bfbc139cf663e1ab4 | | lukso-testnet | LSP8Marketplace | 0x6364738eb197115aece87591dff51d554535d1f8 | | lukso-testnet | Points | 0x3582f474F6E9FB087651b135d6224500A89e6f44 | diff --git a/artifacts/abi/marketplace/lsp8/LSP8Orders.json b/artifacts/abi/marketplace/lsp8/LSP8Orders.json index 3216c49..2fdf63b 100644 --- a/artifacts/abi/marketplace/lsp8/LSP8Orders.json +++ b/artifacts/abi/marketplace/lsp8/LSP8Orders.json @@ -178,6 +178,11 @@ "name": "buyer", "type": "address", "internalType": "address" + }, + { + "name": "tokenIds", + "type": "bytes32[]", + "internalType": "bytes32[]" } ], "outputs": [ @@ -202,6 +207,11 @@ "name": "buyer", "type": "address", "internalType": "address" + }, + { + "name": "tokenIds", + "type": "bytes32[]", + "internalType": "bytes32[]" } ], "outputs": [ @@ -576,6 +586,11 @@ "name": "buyer", "type": "address", "internalType": "address" + }, + { + "name": "tokenIds", + "type": "bytes32[]", + "internalType": "bytes32[]" } ] }, @@ -673,6 +688,11 @@ "name": "buyer", "type": "address", "internalType": "address" + }, + { + "name": "tokenIds", + "type": "bytes32[]", + "internalType": "bytes32[]" } ] }, diff --git a/src/marketplace/lsp8/ILSP8Orders.sol b/src/marketplace/lsp8/ILSP8Orders.sol index f9a4cc2..14f3343 100644 --- a/src/marketplace/lsp8/ILSP8Orders.sol +++ b/src/marketplace/lsp8/ILSP8Orders.sol @@ -45,14 +45,19 @@ interface ILSP8Orders { /// confirms an order has been placed by a buyer /// @param asset asset address /// @param buyer buyer + /// @param tokenIds token ids or empty for any tokens. Must be sorted in ascending order. /// @return true if the order is placed - function isPlacedOrderOf(address asset, address buyer) external view returns (bool); + function isPlacedOrderOf(address asset, address buyer, bytes32[] calldata tokenIds) external view returns (bool); /// retrieves an order for an asset made by a buyer or reverts if not placed /// @param asset asset address /// @param buyer buyer + /// @param tokenIds token ids or empty for any tokens. Must be sorted in ascending order. /// @return order order - function orderOf(address asset, address buyer) external view returns (LSP8Order memory); + function orderOf(address asset, address buyer, bytes32[] calldata tokenIds) + external + view + returns (LSP8Order memory); /// confirms an order has been placed by a buyer /// @param id order id diff --git a/src/marketplace/lsp8/LSP8Orders.sol b/src/marketplace/lsp8/LSP8Orders.sol index 7be3b16..bc39fbb 100644 --- a/src/marketplace/lsp8/LSP8Orders.sol +++ b/src/marketplace/lsp8/LSP8Orders.sol @@ -5,17 +5,17 @@ import {Module} from "../common/Module.sol"; import {ILSP8Orders, LSP8Order} from "./ILSP8Orders.sol"; contract LSP8Orders is ILSP8Orders, Module { - error NotPlacedOf(address asset, address buyer); + error NotPlacedOf(address asset, address buyer, bytes32[] tokenIds); error NotPlaced(uint256 id); error InvalidTokenCount(uint16 tokenCount); - error AlreadyPlaced(address asset, address buyer); + error AlreadyPlaced(address asset, address buyer, bytes32[] tokenIds); error InvalidAmount(uint256 expected, uint256 actual); error Unpaid(address buyer, uint256 amount); error InsufficientTokenCount(uint16 orderCount, uint16 offeredCount); error UnfulfilledToken(bytes32 tokenId); uint256 public totalOrders; - mapping(address asset => mapping(address buyer => uint256 id)) private _orderIds; + mapping(address asset => mapping(address buyer => uint256[] ids)) private _orderIds; mapping(uint256 id => LSP8Order) private _orders; constructor() { @@ -26,15 +26,47 @@ contract LSP8Orders is ILSP8Orders, Module { Module._initialize(newOwner_); } - function isPlacedOrderOf(address asset, address buyer) public view override returns (bool) { - return isPlacedOrder(_orderIds[asset][buyer]); + function _computeTokensKey(bytes32[] memory tokenIds) private pure returns (bytes32) { + bytes32 key = 0; + uint256 length = tokenIds.length; + for (uint256 i = 0; i < length; i++) { + key = keccak256(abi.encodePacked(key, tokenIds[i])); + } + return key; } - function orderOf(address asset, address buyer) external view override returns (LSP8Order memory) { - if (!isPlacedOrderOf(asset, buyer)) { - revert NotPlacedOf(asset, buyer); + function isPlacedOrderOf(address asset, address buyer, bytes32[] memory tokenIds) + public + view + override + returns (bool) + { + uint256[] memory orders = _orderIds[asset][buyer]; + bytes32 tokensKey = _computeTokensKey(tokenIds); + for (uint256 i = 0; i < orders.length; i++) { + LSP8Order memory order = _orders[orders[i]]; + if (order.tokenCount > 0 && _computeTokensKey(order.tokenIds) == tokensKey) { + return true; + } + } + return false; + } + + function orderOf(address asset, address buyer, bytes32[] memory tokenIds) + external + view + override + returns (LSP8Order memory) + { + uint256[] memory orders = _orderIds[asset][buyer]; + bytes32 tokensKey = _computeTokensKey(tokenIds); + for (uint256 i = 0; i < orders.length; i++) { + LSP8Order memory order = _orders[orders[i]]; + if (order.tokenCount > 0 && _computeTokensKey(order.tokenIds) == tokensKey) { + return order; + } } - return _orders[_orderIds[asset][buyer]]; + revert NotPlacedOf(asset, buyer, tokenIds); } function isPlacedOrder(uint256 id) public view override returns (bool) { @@ -66,8 +98,20 @@ contract LSP8Orders is ILSP8Orders, Module { } address buyer = msg.sender; - if (isPlacedOrderOf(asset, buyer)) { - revert AlreadyPlaced(asset, buyer); + + // verify buyer orders do not contain overlapping tokens + { + uint256[] memory orders = _orderIds[asset][buyer]; + for (uint256 i = 0; i < orders.length; i++) { + LSP8Order memory order = _orders[orders[i]]; + for (uint256 j = 0; j < order.tokenIds.length; j++) { + for (uint256 k = 0; k < tokenIds.length; k++) { + if (order.tokenIds[j] == tokenIds[k]) { + revert AlreadyPlaced(asset, buyer, tokenIds); + } + } + } + } } uint256 totalValue = tokenPrice * tokenCount; @@ -78,7 +122,7 @@ contract LSP8Orders is ILSP8Orders, Module { totalOrders += 1; uint256 orderId = totalOrders; - _orderIds[asset][buyer] = orderId; + _orderIds[asset][buyer].push(orderId); _orders[orderId] = LSP8Order({ id: orderId, asset: asset, @@ -97,11 +141,26 @@ contract LSP8Orders is ILSP8Orders, Module { LSP8Order memory order = getOrder(id); if (order.buyer != buyer) { - revert NotPlacedOf(order.asset, buyer); + revert NotPlacedOf(order.asset, buyer, order.tokenIds); } delete _orders[id]; - delete _orderIds[order.asset][buyer]; + + // delete the order id from the buyer's orders + { + uint256[] storage orders = _orderIds[order.asset][order.buyer]; + uint256 index = orders.length; + for (uint256 i = 0; i < orders.length; i++) { + if (orders[i] == id) { + index = i; + break; + } + } + if (index < orders.length) { + orders[index] = orders[orders.length - 1]; + orders.pop(); + } + } uint256 remainingValue = order.tokenPrice * order.tokenCount; (bool success,) = buyer.call{value: remainingValue}(""); @@ -112,7 +171,7 @@ contract LSP8Orders is ILSP8Orders, Module { emit Canceled(id, order.asset, buyer, order.tokenPrice, order.tokenIds, order.tokenCount); } - function fill(uint256 id, address seller, bytes32[] calldata tokenIds) + function fill(uint256 id, address seller, bytes32[] memory tokenIds) external override whenNotPaused diff --git a/test/marketplace/lsp8/LSP8Orders.t.sol b/test/marketplace/lsp8/LSP8Orders.t.sol index d7a48e3..2b02ff4 100644 --- a/test/marketplace/lsp8/LSP8Orders.t.sol +++ b/test/marketplace/lsp8/LSP8Orders.t.sol @@ -104,10 +104,10 @@ contract LSP8OrdersTest is Test { orders.fill(1, address(100), tokenIds); } - function testFuzz_NotPlacedOf(address someAsset, address buyer) public { - assertFalse(orders.isPlacedOrderOf(someAsset, buyer)); - vm.expectRevert(abi.encodeWithSelector(LSP8Orders.NotPlacedOf.selector, someAsset, buyer)); - orders.orderOf(someAsset, buyer); + function testFuzz_NotPlacedOf(address someAsset, address buyer, bytes32[] memory tokenIds) public { + assertFalse(orders.isPlacedOrderOf(someAsset, buyer, tokenIds)); + vm.expectRevert(abi.encodeWithSelector(LSP8Orders.NotPlacedOf.selector, someAsset, buyer, tokenIds)); + orders.orderOf(someAsset, buyer, tokenIds); } function testFuzz_NotPlaced(uint256 id) public { @@ -131,9 +131,9 @@ contract LSP8OrdersTest is Test { orders.place{value: tokenPrice * tokenCount}(address(asset), tokenPrice, tokenIds, tokenCount); assertTrue(orders.isPlacedOrder(1)); - assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), tokenIds)); - LSP8Order memory order = orders.orderOf(address(asset), address(alice)); + LSP8Order memory order = orders.orderOf(address(asset), address(alice), tokenIds); assertEq(abi.encode(order), abi.encode(orders.getOrder(1))); assertEq(order.id, 1); assertEq(order.asset, address(asset)); @@ -143,6 +143,98 @@ contract LSP8OrdersTest is Test { assertEq(order.tokenCount, tokenCount); } + function test_PlaceMultipleForAnyAndSomeTokens() public { + bytes32[] memory firstTokenIds = new bytes32[](0); + + bytes32[] memory secondTokenIds = new bytes32[](3); + secondTokenIds[0] = keccak256("token1"); + secondTokenIds[1] = keccak256("token2"); + secondTokenIds[2] = keccak256("token3"); + + (UniversalProfile alice,) = deployProfile(); + + vm.deal(address(alice), 8 ether); + + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(1, address(asset), address(alice), 1 ether, firstTokenIds, 5); + orders.place{value: 5 ether}(address(asset), 1 ether, firstTokenIds, 5); + + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(2, address(asset), address(alice), 1 ether, secondTokenIds, 3); + orders.place{value: 3 ether}(address(asset), 1 ether, secondTokenIds, 3); + + assertTrue(orders.isPlacedOrder(1)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), firstTokenIds)); + + assertTrue(orders.isPlacedOrder(2)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), secondTokenIds)); + } + + function test_PlaceMultipleForDifferentTokens() public { + bytes32[] memory firstTokenIds = new bytes32[](2); + firstTokenIds[0] = keccak256("token1"); + firstTokenIds[1] = keccak256("token2"); + + bytes32[] memory secondTokenIds = new bytes32[](3); + secondTokenIds[0] = keccak256("token3"); + secondTokenIds[1] = keccak256("token4"); + secondTokenIds[2] = keccak256("token5"); + + (UniversalProfile alice,) = deployProfile(); + + vm.deal(address(alice), 5 ether); + + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(1, address(asset), address(alice), 1 ether, firstTokenIds, 2); + orders.place{value: 2 ether}(address(asset), 1 ether, firstTokenIds, 2); + + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(2, address(asset), address(alice), 1 ether, secondTokenIds, 3); + orders.place{value: 3 ether}(address(asset), 1 ether, secondTokenIds, 3); + + assertTrue(orders.isPlacedOrder(1)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), firstTokenIds)); + + assertTrue(orders.isPlacedOrder(2)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), secondTokenIds)); + } + + function test_Revert_PlaceMultipleForOverlappingTokens() public { + bytes32[] memory firstTokenIds = new bytes32[](2); + firstTokenIds[0] = keccak256("token1"); + firstTokenIds[1] = keccak256("token2"); + + bytes32[] memory secondTokenIds = new bytes32[](3); + secondTokenIds[0] = keccak256("token3"); + secondTokenIds[1] = keccak256("token1"); + secondTokenIds[2] = keccak256("token4"); + + (UniversalProfile alice,) = deployProfile(); + + vm.deal(address(alice), 5 ether); + + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(1, address(asset), address(alice), 1 ether, firstTokenIds, 2); + orders.place{value: 2 ether}(address(asset), 1 ether, firstTokenIds, 2); + + vm.prank(address(alice)); + vm.expectRevert( + abi.encodeWithSelector(LSP8Orders.AlreadyPlaced.selector, address(asset), address(alice), secondTokenIds) + ); + orders.place{value: 3 ether}(address(asset), 1 ether, secondTokenIds, 3); + + assertTrue(orders.isPlacedOrder(1)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), firstTokenIds)); + + assertFalse(orders.isPlacedOrder(2)); + assertFalse(orders.isPlacedOrderOf(address(asset), address(alice), secondTokenIds)); + } + function test_Revert_InvalidTokenCount() public { (UniversalProfile alice,) = deployProfile(); vm.deal(address(alice), 100 ether); @@ -186,7 +278,9 @@ contract LSP8OrdersTest is Test { orders.place{value: 1 ether}(address(asset), 1 ether, tokenIds, 1); vm.prank(address(alice)); - vm.expectRevert(abi.encodeWithSelector(LSP8Orders.AlreadyPlaced.selector, address(asset), address(alice))); + vm.expectRevert( + abi.encodeWithSelector(LSP8Orders.AlreadyPlaced.selector, address(asset), address(alice), tokenIds) + ); orders.place{value: 1 ether}(address(asset), 1 ether, tokenIds, 1); } @@ -218,7 +312,7 @@ contract LSP8OrdersTest is Test { orders.cancel(1); assertFalse(orders.isPlacedOrder(1)); - assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); + assertFalse(orders.isPlacedOrderOf(address(asset), address(alice), tokenIds)); assertEq(address(alice).balance, 1 ether); } @@ -233,14 +327,14 @@ contract LSP8OrdersTest is Test { vm.prank(address(alice)); orders.place{value: 1 ether}(address(asset), 1 ether, tokenIds, 1); - assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), tokenIds)); assertEq(address(alice).balance, 0 ether); vm.prank(address(bob)); - vm.expectRevert(abi.encodeWithSelector(LSP8Orders.NotPlacedOf.selector, address(asset), address(bob))); + vm.expectRevert(abi.encodeWithSelector(LSP8Orders.NotPlacedOf.selector, address(asset), address(bob), tokenIds)); orders.cancel(1); - assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), tokenIds)); assertEq(address(alice).balance, 0 ether); } @@ -289,7 +383,6 @@ contract LSP8OrdersTest is Test { if (fillCount == tokenCount) { assertFalse(orders.isPlacedOrder(1)); - assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); } else { LSP8Order memory order = orders.getOrder(1); assertEq(order.tokenIds.length, tokenCount - fillCount); @@ -369,7 +462,7 @@ contract LSP8OrdersTest is Test { orders.grantRole(marketplace, MARKETPLACE_ROLE); assertTrue(orders.isPlacedOrder(1)); - assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), tokenIds)); assertEq(address(alice).balance, 0 ether); assertEq(address(bob).balance, 0 ether); assertEq(marketplace.balance, 0 ether); @@ -381,7 +474,7 @@ contract LSP8OrdersTest is Test { orders.fill(1, address(bob), fillTokenIds); assertTrue(orders.isPlacedOrder(1)); - assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice), cancelTokenIds)); assertEq(address(alice).balance, 0 ether); assertEq(address(bob).balance, 0 ether); assertEq(marketplace.balance, 1 ether); @@ -393,7 +486,6 @@ contract LSP8OrdersTest is Test { orders.cancel(1); assertFalse(orders.isPlacedOrder(1)); - assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); assertEq(address(alice).balance, 1 ether); assertEq(address(bob).balance, 0 ether); assertEq(marketplace.balance, 1 ether);