diff --git a/README.md b/README.md index 3dc8a91..9af0464 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ | mainnet | Participant | 0xa29aeaabb5da0cc3635576933a66c1b714f058c1 | | mainnet | LSP7Listings | 0xe7f5c709d62bcc3701f4c0cb871eb77e301283b5 | | mainnet | LSP7Offers | 0xb2379f3f3c623cd2ed18e97e407cdda8fe6c6da6 | +| mainnet | LSP7Orders | | | mainnet | LSP7Marketplace | 0xe04cf97440cd191096c4103f9c48abd96184fb8d | | mainnet | LSP8Listings | 0x4faab47b234c7f5da411429ee86cb15cb0754354 | | mainnet | LSP8Offers | 0xed189b51455c9714aa49b0c55529469c512b10b6 | @@ -25,6 +26,7 @@ | testnet | Participant | 0x5a485297a1b909032a6b7000354f3322047028ee | | testnet | LSP7Listings | 0x44cd7d06ceb509370b75e426ea3c12824a665e36 | | testnet | LSP7Offers | 0xdf9defd55365b7b073cae009cf53dd830902c5a7 | +| testnet | LSP7Orders | 0xf030798a7b2722c32b58fae3aee5019989cd409f | | testnet | LSP7Marketplace | 0xc9c940a35fc8d3522085b991ce3e1a920354f19a | | testnet | LSP8Listings | 0xf069f9b8e0f96d742c6dfd3d78b0e382f3411207 | | testnet | LSP8Offers | 0xaebcc2c80abacb7e4d928d4c0a52c7bbeba4c4be | diff --git a/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json b/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json index 116c939..b2ae86f 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json @@ -129,11 +129,6 @@ }, { "inputs": [ - { - "internalType": "uint256", - "name": "listingId", - "type": "uint256" - }, { "internalType": "address", "name": "account", @@ -318,6 +313,43 @@ "name": "RoyaltiesPaid", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalPaid", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RoyaltiesPaidOut", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -380,6 +412,61 @@ "name": "Sale", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "seller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalPaid", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalRoyalties", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "Sold", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -498,6 +585,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "orderId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "fillOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -520,6 +625,11 @@ "name": "offers_", "type": "address" }, + { + "internalType": "contract ILSP7Orders", + "name": "orders_", + "type": "address" + }, { "internalType": "contract IParticipant", "name": "participant_", @@ -557,6 +667,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "orders", + "outputs": [ + { + "internalType": "contract ILSP7Orders", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -649,6 +772,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "orders_", + "type": "address" + } + ], + "name": "setOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/artifacts/abi/marketplace/lsp7/LSP7Orders.json b/artifacts/abi/marketplace/lsp7/LSP7Orders.json new file mode 100644 index 0000000..6cfb4c2 --- /dev/null +++ b/artifacts/abi/marketplace/lsp7/LSP7Orders.json @@ -0,0 +1,684 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "AlreadyPlaced", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "IllegalAccess", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "orderCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offeredCount", + "type": "uint256" + } + ], + "name": "InsufficientItemCount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "InvalidAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "InvalidItemCount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "NotPlaced", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "NotPlacedOf", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "callerAddress", + "type": "address" + } + ], + "name": "OwnableCallerNotTheOwner", + "type": "error" + }, + { + "inputs": [], + "name": "OwnableCannotSetZeroAddressAsOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Unpaid", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "Canceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "seller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fillCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalCount", + "type": "uint256" + } + ], + "name": "Filled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "Placed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "cancel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "seller", + "type": "address" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "fill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "getOrder", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "internalType": "struct LSP7Order", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "isPlacedOrder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "isPlacedOrderOf", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "orderOf", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "internalType": "struct LSP7Order", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "place", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalOrders", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/marketplace/Participant.sol b/src/marketplace/Participant.sol index 1bd24df..11431ef 100644 --- a/src/marketplace/Participant.sol +++ b/src/marketplace/Participant.sol @@ -9,10 +9,10 @@ import {ICollectorIdentifiableDigitalAsset} from "../assets/lsp8/ICollectorIdent import {IParticipant} from "./IParticipant.sol"; uint32 constant GENESIS_DISCOUNT = 20_000; -uint32 constant COLLECTOR_TIER_0_DISCOUNT = 25_000; +uint32 constant COLLECTOR_TIER_0_DISCOUNT = 35_000; uint32 constant COLLECTOR_TIER_1_DISCOUNT = 50_000; uint32 constant COLLECTOR_TIER_2_DISCOUNT = 75_000; -uint32 constant COLLECTOR_TIER_3_DISCOUNT = 100_000; +uint32 constant COLLECTOR_TIER_3_DISCOUNT = 90_000; contract Participant is IParticipant, OwnableUnset, PausableUpgradeable { event AssetFeeDiscountChanged(address indexed asset, uint32 previousDiscountPoints, uint32 newDiscountPoints); diff --git a/src/marketplace/lsp7/ILSP7Orders.sol b/src/marketplace/lsp7/ILSP7Orders.sol new file mode 100644 index 0000000..724d3ea --- /dev/null +++ b/src/marketplace/lsp7/ILSP7Orders.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +// Order for LSP7 asset +struct LSP7Order { + uint256 id; + address asset; + address buyer; + uint256 itemPrice; + uint256 itemCount; +} + +interface ILSP7Orders { + /// an order was made for an asset + event Placed(uint256 id, address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); + /// a buyer canceled an order + event Canceled(uint256 id, address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); + /// an order was filled + event Filled( + uint256 id, + address indexed asset, + address indexed seller, + address indexed buyer, + uint256 itemPrice, + uint256 fillCount, + uint256 totalCount + ); + + /// confirms an order has been placed by a buyer + /// @param asset asset address + /// @param buyer buyer + function isPlacedOrderOf(address asset, address buyer) 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 + function orderOf(address asset, address buyer) external view returns (LSP7Order memory); + + /// confirms an order has been placed by a buyer + /// @param id order id + function isPlacedOrder(uint256 id) external view returns (bool); + + /// retrieves an order for an asset reverts if not placed + /// @param id order id + function getOrder(uint256 id) external view returns (LSP7Order memory); + + /// place an offer order with a fixed item price, number of items and seconds until the offer is expired. + /// @param asset asset address + /// @param itemPrice item price + /// @param itemCount number of items + /// @return orderId order id + function place(address asset, uint256 itemPrice, uint256 itemCount) external payable returns (uint256); + + /// cancel an order by a buyer being a sender. + /// @param id order id + function cancel(uint256 id) external; + + /// fill an order. + /// @param id order id + /// @param seller seller + /// @param itemCount number of items + function fill(uint256 id, address seller, uint256 itemCount) external; +} diff --git a/src/marketplace/lsp7/LSP7Marketplace.sol b/src/marketplace/lsp7/LSP7Marketplace.sol index 06490e4..99b1679 100644 --- a/src/marketplace/lsp7/LSP7Marketplace.sol +++ b/src/marketplace/lsp7/LSP7Marketplace.sol @@ -6,28 +6,36 @@ import {Base} from "../common/Base.sol"; import {IParticipant} from "../IParticipant.sol"; import {ILSP7Listings, LSP7Listing} from "./ILSP7Listings.sol"; import {ILSP7Offers, LSP7Offer} from "./ILSP7Offers.sol"; +import {ILSP7Orders, LSP7Order} from "./ILSP7Orders.sol"; + +uint8 constant SALE_KIND_MASK = 0xF; +uint8 constant SALE_KIND_SPOT = 0; +uint8 constant SALE_KIND_OFFER = 1; +uint8 constant SALE_KIND_ORDER = 2; contract LSP7Marketplace is Base { - event Sale( - uint256 indexed listingId, + event Sold( address indexed asset, - uint256 itemCount, address indexed seller, - address buyer, - uint256 totalPaid + address indexed buyer, + uint256 itemCount, + uint256 totalPaid, + uint256 totalFee, + uint256 totalRoyalties, + bytes data ); - event RoyaltiesPaid( - uint256 indexed listingId, address indexed asset, uint256 itemCount, address indexed recipient, uint256 amount + event RoyaltiesPaidOut( + address indexed asset, uint256 itemCount, uint256 totalPaid, address indexed recipient, uint256 amount ); - event FeePaid(uint256 indexed listingId, address indexed asset, uint256 itemCount, uint256 amount); error InsufficientFunds(uint256 totalPrice, uint256 totalPaid); error FeesExceedTotalPaid(uint256 totalPaid, uint256 feesAmount, uint256 royaltiesTotalAmount); - error Unpaid(uint256 listingId, address account, uint256 amount); + error Unpaid(address account, uint256 amount); error UnathorizedSeller(address account); ILSP7Listings public listings; ILSP7Offers public offers; + ILSP7Orders public orders; constructor() { _disableInitializers(); @@ -38,13 +46,17 @@ contract LSP7Marketplace is Base { address beneficiary_, ILSP7Listings listings_, ILSP7Offers offers_, + ILSP7Orders orders_, IParticipant participant_ ) external initializer { - require(address(listings_) != address(0)); - require(address(offers_) != address(0)); Base._initialize(newOwner_, beneficiary_, participant_); listings = listings_; offers = offers_; + orders = orders_; + } + + function setOrders(address orders_) external onlyOwner { + orders = ILSP7Orders(orders_); } function buy(uint256 listingId, uint256 itemCount, address recipient) external payable whenNotPaused nonReentrant { @@ -53,7 +65,9 @@ contract LSP7Marketplace is Base { revert InsufficientFunds(listing.itemPrice * itemCount, msg.value); } listings.deduct(listingId, itemCount); - _executeSale(listingId, listing.asset, itemCount, listing.owner, recipient, msg.value); + _executeSale( + listing.asset, itemCount, listing.owner, recipient, msg.value, abi.encodePacked(SALE_KIND_SPOT, listingId) + ); } function acceptOffer(uint256 listingId, address buyer) external whenNotPaused nonReentrant { @@ -64,16 +78,37 @@ contract LSP7Marketplace is Base { LSP7Offer memory offer = offers.getOffer(listingId, buyer); offers.accept(listingId, buyer); listings.deduct(listingId, offer.itemCount); - _executeSale(listingId, listing.asset, offer.itemCount, listing.owner, buyer, offer.totalPrice); + _executeSale( + listing.asset, + offer.itemCount, + listing.owner, + buyer, + offer.totalPrice, + abi.encodePacked(SALE_KIND_OFFER, listingId) + ); + } + + function fillOrder(uint256 orderId, uint256 itemCount) external whenNotPaused nonReentrant { + address seller = msg.sender; + LSP7Order memory order = orders.getOrder(orderId); + orders.fill(orderId, seller, itemCount); + _executeSale( + order.asset, + itemCount, + seller, + order.buyer, + order.itemPrice * itemCount, + abi.encodePacked(SALE_KIND_ORDER, order.id) + ); } function _executeSale( - uint256 listingId, address asset, uint256 itemCount, address seller, address buyer, - uint256 totalPaid + uint256 totalPaid, + bytes memory data ) private { (uint256 royaltiesTotalAmount, address[] memory royaltiesRecipients, uint256[] memory royaltiesAmounts) = _calculateRoyalties(asset, totalPaid); @@ -81,25 +116,36 @@ contract LSP7Marketplace is Base { if (feeAmount + royaltiesTotalAmount > totalPaid) { revert FeesExceedTotalPaid(totalPaid, feeAmount, royaltiesTotalAmount); } + emit Sold(asset, seller, buyer, itemCount, totalPaid, feeAmount, royaltiesTotalAmount, data); uint256 royaltiesRecipientsCount = royaltiesRecipients.length; for (uint256 i = 0; i < royaltiesRecipientsCount; i++) { if (royaltiesAmounts[i] > 0) { (bool royaltiesPaid,) = royaltiesRecipients[i].call{value: royaltiesAmounts[i]}(""); if (!royaltiesPaid) { - revert Unpaid(listingId, royaltiesRecipients[i], royaltiesAmounts[i]); + revert Unpaid(royaltiesRecipients[i], royaltiesAmounts[i]); } - emit RoyaltiesPaid(listingId, asset, itemCount, royaltiesRecipients[i], royaltiesAmounts[i]); + emit RoyaltiesPaidOut(asset, itemCount, totalPaid, royaltiesRecipients[i], royaltiesAmounts[i]); } } uint256 sellerAmount = totalPaid - feeAmount - royaltiesTotalAmount; (bool paid,) = seller.call{value: sellerAmount}(""); if (!paid) { - revert Unpaid(listingId, seller, sellerAmount); + revert Unpaid(seller, sellerAmount); } - if (feeAmount > 0) { - emit FeePaid(listingId, asset, itemCount, feeAmount); - } - ILSP7DigitalAsset(asset).transfer(seller, buyer, itemCount, false, ""); - emit Sale(listingId, asset, itemCount, seller, buyer, totalPaid); + ILSP7DigitalAsset(asset).transfer(seller, buyer, itemCount, false, data); } + + // Deprecated events + event Sale( + uint256 indexed listingId, + address indexed asset, + uint256 itemCount, + address indexed seller, + address buyer, + uint256 totalPaid + ); + event RoyaltiesPaid( + uint256 indexed listingId, address indexed asset, uint256 itemCount, address indexed recipient, uint256 amount + ); + event FeePaid(uint256 indexed listingId, address indexed asset, uint256 itemCount, uint256 amount); } diff --git a/src/marketplace/lsp7/LSP7Orders.sol b/src/marketplace/lsp7/LSP7Orders.sol new file mode 100644 index 0000000..813ccab --- /dev/null +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +import {Module} from "../common/Module.sol"; +import {ILSP7Orders, LSP7Order} from "./ILSP7Orders.sol"; + +contract LSP7Orders is ILSP7Orders, Module { + error Unpaid(address buyer, uint256 amount); + error NotPlacedOf(address asset, address buyer); + error NotPlaced(uint256 id); + error InvalidItemCount(uint256 itemCount); + error AlreadyPlaced(address asset, address buyer); + error InvalidAmount(uint256 expected, uint256 actual); + error InsufficientItemCount(uint256 orderCount, uint256 offeredCount); + + uint256 public totalOrders; + mapping(address asset => mapping(address buyer => uint256)) private _orderIds; + mapping(uint256 id => LSP7Order) private _orders; + + constructor() { + _disableInitializers(); + } + + function initialize(address newOwner_) external initializer { + Module._initialize(newOwner_); + } + + function isPlacedOrderOf(address asset, address buyer) public view override returns (bool) { + return isPlacedOrder(_orderIds[asset][buyer]); + } + + function orderOf(address asset, address buyer) public view override returns (LSP7Order memory) { + if (!isPlacedOrderOf(asset, buyer)) { + revert NotPlacedOf(asset, buyer); + } + return _orders[_orderIds[asset][buyer]]; + } + + function isPlacedOrder(uint256 id) public view override returns (bool) { + LSP7Order memory order = _orders[id]; + return order.itemCount > 0; + } + + function getOrder(uint256 id) public view override returns (LSP7Order memory) { + if (!isPlacedOrder(id)) { + revert NotPlaced(id); + } + return _orders[id]; + } + + function place(address asset, uint256 itemPrice, uint256 itemCount) + external + payable + override + whenNotPaused + nonReentrant + returns (uint256) + { + if (itemCount == 0) { + revert InvalidItemCount(itemCount); + } + address buyer = msg.sender; + if (isPlacedOrderOf(asset, buyer)) { + revert AlreadyPlaced(asset, buyer); + } + uint256 totalValue = itemPrice * itemCount; + if (msg.value != totalValue) { + revert InvalidAmount(totalValue, msg.value); + } + totalOrders += 1; + uint256 orderId = totalOrders; + _orderIds[asset][buyer] = orderId; + _orders[orderId] = + LSP7Order({id: orderId, asset: asset, buyer: buyer, itemPrice: itemPrice, itemCount: itemCount}); + emit Placed(orderId, asset, buyer, itemPrice, itemCount); + return orderId; + } + + function cancel(uint256 id) external override whenNotPaused nonReentrant { + address buyer = msg.sender; + LSP7Order memory order = getOrder(id); + if (order.buyer != buyer) { + revert NotPlacedOf(order.asset, buyer); + } + delete _orders[id]; + delete _orderIds[order.asset][order.buyer]; + uint256 totalValue = order.itemPrice * order.itemCount; + (bool success,) = order.buyer.call{value: totalValue}(""); + if (!success) { + revert Unpaid(order.buyer, totalValue); + } + emit Canceled(order.id, order.asset, order.buyer, order.itemPrice, order.itemCount); + } + + function fill(uint256 id, address seller, uint256 itemCount) + external + override + whenNotPaused + nonReentrant + onlyMarketplace + { + if (itemCount == 0) { + revert InvalidItemCount(itemCount); + } + LSP7Order memory order = getOrder(id); + if (itemCount > order.itemCount) { + revert InsufficientItemCount(order.itemCount, itemCount); + } + _orders[order.id].itemCount -= itemCount; + uint256 totalValue = order.itemPrice * itemCount; + (bool success,) = msg.sender.call{value: totalValue}(""); + if (!success) { + revert Unpaid(msg.sender, totalValue); + } + emit Filled(order.id, order.asset, seller, order.buyer, order.itemPrice, itemCount, order.itemCount); + } +} diff --git a/test/marketplace/lsp7/LSP7Marketplace.t.sol b/test/marketplace/lsp7/LSP7Marketplace.t.sol index 9690d11..d8f6c4b 100644 --- a/test/marketplace/lsp7/LSP7Marketplace.t.sol +++ b/test/marketplace/lsp7/LSP7Marketplace.t.sol @@ -16,6 +16,7 @@ import {Royalties, RoyaltiesInfo} from "../../../src/common/Royalties.sol"; import {Base} from "../../../src/marketplace/common/Base.sol"; import {LSP7Listings} from "../../../src/marketplace/lsp7/LSP7Listings.sol"; import {LSP7Offers} from "../../../src/marketplace/lsp7/LSP7Offers.sol"; +import {LSP7Orders} from "../../../src/marketplace/lsp7/LSP7Orders.sol"; import {LSP7Marketplace} from "../../../src/marketplace/lsp7/LSP7Marketplace.sol"; import {Participant, GENESIS_DISCOUNT} from "../../../src/marketplace/Participant.sol"; import {deployProfile} from "../../utils/profile.sol"; @@ -24,22 +25,24 @@ import {LSP7DigitalAssetMock} from "./LSP7DigitalAssetMock.sol"; contract LSP7MarketplaceTest is Test { event FeePointsChanged(uint32 oldPoints, uint32 newPoints); event RoyaltiesThresholdPointsChanged(uint32 oldPoints, uint32 newPoints); - event Sale( - uint256 indexed listingId, + event Sold( address indexed asset, - uint256 itemCount, address indexed seller, - address buyer, - uint256 totalPaid + address indexed buyer, + uint256 itemCount, + uint256 totalPaid, + uint256 totalFee, + uint256 totalRoyalties, + bytes data ); - event RoyaltiesPaid( - uint256 indexed listingId, address indexed asset, uint256 itemCount, address indexed recipient, uint256 amount + event RoyaltiesPaidOut( + address indexed asset, uint256 itemCount, uint256 totalPaid, address indexed recipient, uint256 amount ); - event FeePaid(uint256 indexed listingId, address indexed asset, uint256 itemCount, uint256 amount); event ValueWithdrawn(address indexed beneficiary, uint256 indexed value); LSP7Listings listings; LSP7Offers offers; + LSP7Orders orders; LSP7Marketplace marketplace; Participant participant; address admin; @@ -81,6 +84,15 @@ contract LSP7MarketplaceTest is Test { ) ) ); + orders = LSP7Orders( + address( + new TransparentUpgradeableProxy( + address(new LSP7Orders()), + admin, + abi.encodeWithSelector(LSP7Orders.initialize.selector, owner) + ) + ) + ); marketplace = LSP7Marketplace( payable( address( @@ -88,7 +100,7 @@ contract LSP7MarketplaceTest is Test { address(new LSP7Marketplace()), admin, abi.encodeWithSelector( - LSP7Marketplace.initialize.selector, owner, beneficiary, listings, offers, participant + LSP7Marketplace.initialize.selector, owner, beneficiary, listings, offers, orders, participant ) ) ) @@ -98,10 +110,12 @@ contract LSP7MarketplaceTest is Test { vm.startPrank(owner); listings.grantRole(address(marketplace), MARKETPLACE_ROLE); offers.grantRole(address(marketplace), MARKETPLACE_ROLE); + orders.grantRole(address(marketplace), MARKETPLACE_ROLE); vm.stopPrank(); assertTrue(listings.hasRole(address(marketplace), MARKETPLACE_ROLE)); assertTrue(offers.hasRole(address(marketplace), MARKETPLACE_ROLE)); + assertTrue(orders.hasRole(address(marketplace), MARKETPLACE_ROLE)); } function test_Initialized() public { @@ -110,6 +124,7 @@ contract LSP7MarketplaceTest is Test { assertEq(beneficiary, marketplace.beneficiary()); assertEq(address(listings), address(marketplace.listings())); assertEq(address(offers), address(marketplace.offers())); + assertEq(address(orders), address(marketplace.orders())); assertEq(address(participant), address(marketplace.participant())); assertEq(0, marketplace.feePoints()); assertEq(0, marketplace.royaltiesThresholdPoints()); @@ -181,7 +196,16 @@ contract LSP7MarketplaceTest is Test { vm.prank(address(bob)); vm.expectEmit(address(marketplace)); - emit Sale(1, address(asset), itemCount, address(alice), address(bob), totalPrice); + emit Sold( + address(asset), + address(alice), + address(bob), + itemCount, + totalPrice, + 0, + 0, + hex"000000000000000000000000000000000000000000000000000000000000000001" + ); marketplace.buy{value: totalPrice}(1, itemCount, address(bob)); assertFalse(listings.isListed(1)); @@ -213,12 +237,17 @@ contract LSP7MarketplaceTest is Test { vm.deal(address(bob), totalPrice); vm.prank(address(bob)); - if (feeAmount > 0) { - vm.expectEmit(address(marketplace)); - emit FeePaid(1, address(asset), itemCount, feeAmount); - } vm.expectEmit(address(marketplace)); - emit Sale(1, address(asset), itemCount, address(alice), address(bob), totalPrice); + emit Sold( + address(asset), + address(alice), + address(bob), + itemCount, + totalPrice, + feeAmount, + 0, + hex"000000000000000000000000000000000000000000000000000000000000000001" + ); marketplace.buy{value: totalPrice}(1, itemCount, address(bob)); assertFalse(listings.isListed(1)); @@ -254,12 +283,17 @@ contract LSP7MarketplaceTest is Test { vm.deal(address(bob), totalPrice); vm.prank(address(bob)); - if (feeAmount > 0) { - vm.expectEmit(address(marketplace)); - emit FeePaid(1, address(asset), 10, feeAmount - discountFeeAmount); - } vm.expectEmit(address(marketplace)); - emit Sale(1, address(asset), 10, address(alice), address(bob), totalPrice); + emit Sold( + address(asset), + address(alice), + address(bob), + 10, + totalPrice, + feeAmount - discountFeeAmount, + 0, + hex"000000000000000000000000000000000000000000000000000000000000000001" + ); marketplace.buy{value: totalPrice}(1, 10, address(bob)); assertFalse(listings.isListed(1)); @@ -305,16 +339,25 @@ contract LSP7MarketplaceTest is Test { vm.deal(address(bob), totalPrice); vm.prank(address(bob)); + vm.expectEmit(address(marketplace)); + emit Sold( + address(asset), + address(alice), + address(bob), + itemCount, + totalPrice, + 0, + royaltiesAmount0 + royaltiesAmount1, + hex"000000000000000000000000000000000000000000000000000000000000000001" + ); if (royaltiesAmount0 > 0) { vm.expectEmit(address(marketplace)); - emit RoyaltiesPaid(1, address(asset), itemCount, royaltiesRecipient0, royaltiesAmount0); + emit RoyaltiesPaidOut(address(asset), itemCount, totalPrice, royaltiesRecipient0, royaltiesAmount0); } if (royaltiesAmount1 > 0) { vm.expectEmit(address(marketplace)); - emit RoyaltiesPaid(1, address(asset), itemCount, royaltiesRecipient1, royaltiesAmount1); + emit RoyaltiesPaidOut(address(asset), itemCount, totalPrice, royaltiesRecipient1, royaltiesAmount1); } - vm.expectEmit(address(marketplace)); - emit Sale(1, address(asset), itemCount, address(alice), address(bob), totalPrice); marketplace.buy{value: totalPrice}(1, itemCount, address(bob)); assertFalse(listings.isListed(1)); @@ -343,9 +386,16 @@ contract LSP7MarketplaceTest is Test { vm.deal(address(bob), 10 ether); vm.prank(address(bob)); vm.expectEmit(address(marketplace)); - emit FeePaid(1, address(asset), 10, 1 ether); - vm.expectEmit(address(marketplace)); - emit Sale(1, address(asset), 10, address(alice), address(bob), 10 ether); + emit Sold( + address(asset), + address(alice), + address(bob), + 10, + 10 ether, + 1 ether, + 0, + hex"000000000000000000000000000000000000000000000000000000000000000001" + ); marketplace.buy{value: 10 ether}(1, 10, address(bob)); assertFalse(listings.isListed(1)); @@ -407,7 +457,16 @@ contract LSP7MarketplaceTest is Test { vm.prank(address(alice)); vm.expectEmit(address(marketplace)); - emit Sale(1, address(asset), itemCount, address(alice), address(bob), totalPrice); + emit Sold( + address(asset), + address(alice), + address(bob), + itemCount, + totalPrice, + 0, + 0, + hex"010000000000000000000000000000000000000000000000000000000000000001" + ); marketplace.acceptOffer(1, address(bob)); assertFalse(listings.isListed(1)); @@ -416,4 +475,46 @@ contract LSP7MarketplaceTest is Test { assertEq(asset.balanceOf(address(alice)), 0); assertEq(asset.balanceOf(address(bob)), itemCount); } + + function testFuzz_FillOrder(uint256 itemCount, uint256 itemPrice, uint256 fillCount) public { + vm.assume(itemPrice < 100_000_000 ether); + vm.assume(itemCount > 0 && itemCount < 1_000_000); + vm.assume(fillCount > 0 && fillCount < itemCount); + + (UniversalProfile alice,) = deployProfile(); + (UniversalProfile bob,) = deployProfile(); + + vm.deal(address(alice), itemPrice * itemCount); + vm.prank(address(alice)); + uint256 orderId = orders.place{value: itemPrice * itemCount}(address(asset), itemPrice, itemCount); + assertEq(orderId, 1); + + asset.mint(address(bob), itemCount, false, ""); + vm.prank(address(bob)); + asset.authorizeOperator(address(marketplace), itemCount, ""); + + assertEq(address(alice).balance, 0); + assertEq(address(bob).balance, 0); + assertEq(asset.balanceOf(address(alice)), 0); + assertEq(asset.balanceOf(address(bob)), itemCount); + + vm.prank(address(bob)); + vm.expectEmit(); + emit Sold( + address(asset), + address(bob), + address(alice), + fillCount, + itemPrice * fillCount, + 0, + 0, + hex"020000000000000000000000000000000000000000000000000000000000000001" + ); + marketplace.fillOrder(orderId, fillCount); + + assertEq(address(alice).balance, 0); + assertEq(address(bob).balance, itemPrice * fillCount); + assertEq(asset.balanceOf(address(alice)), fillCount); + assertEq(asset.balanceOf(address(bob)), itemCount - fillCount); + } } diff --git a/test/marketplace/lsp7/LSP7Orders.t.sol b/test/marketplace/lsp7/LSP7Orders.t.sol new file mode 100644 index 0000000..1504d8c --- /dev/null +++ b/test/marketplace/lsp7/LSP7Orders.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +import {Test} from "forge-std/Test.sol"; +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {UniversalProfile} from "@lukso/lsp-smart-contracts/contracts/UniversalProfile.sol"; +import {_LSP4_TOKEN_TYPE_NFT} from "@lukso/lsp-smart-contracts/contracts/LSP4DigitalAssetMetadata/LSP4Constants.sol"; +import {OwnableCallerNotTheOwner} from "@erc725/smart-contracts/contracts/errors.sol"; +import {Module, MARKETPLACE_ROLE} from "../../../src/marketplace/common/Module.sol"; +import {LSP7Listings, LSP7Listing} from "../../../src/marketplace/lsp7/LSP7Listings.sol"; +import {LSP7Orders, LSP7Order} from "../../../src/marketplace/lsp7/LSP7Orders.sol"; +import {deployProfile} from "../../utils/profile.sol"; +import {LSP7DigitalAssetMock} from "./LSP7DigitalAssetMock.sol"; + +contract LSP7OrdersTest is Test { + event Placed(uint256 id, address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); + event Canceled(uint256 id, address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); + event Filled( + uint256 id, + address indexed asset, + address indexed seller, + address indexed buyer, + uint256 itemPrice, + uint256 fillCount, + uint256 totalCount + ); + + LSP7Orders orders; + address admin; + address owner; + LSP7DigitalAssetMock asset; + + function setUp() public { + admin = vm.addr(1); + owner = vm.addr(2); + + asset = new LSP7DigitalAssetMock("Mock", "MCK", owner, _LSP4_TOKEN_TYPE_NFT, true); + + orders = LSP7Orders( + address( + new TransparentUpgradeableProxy( + address(new LSP7Orders()), + admin, + abi.encodeWithSelector(LSP7Orders.initialize.selector, owner) + ) + ) + ); + } + + function test_Initialized() public { + assertTrue(!orders.paused()); + assertEq(owner, orders.owner()); + } + + function test_ConfigureIfOwner() public { + vm.startPrank(owner); + orders.pause(); + orders.unpause(); + vm.stopPrank(); + } + + function test_Revert_IfConfigureNotOwner() public { + vm.prank(address(1)); + vm.expectRevert(abi.encodeWithSelector(OwnableCallerNotTheOwner.selector, address(1))); + orders.grantRole(address(100), MARKETPLACE_ROLE); + + vm.prank(address(1)); + vm.expectRevert(abi.encodeWithSelector(OwnableCallerNotTheOwner.selector, address(1))); + orders.revokeRole(address(100), MARKETPLACE_ROLE); + + vm.prank(address(1)); + vm.expectRevert(abi.encodeWithSelector(OwnableCallerNotTheOwner.selector, address(1))); + orders.pause(); + + vm.prank(address(1)); + vm.expectRevert(abi.encodeWithSelector(OwnableCallerNotTheOwner.selector, address(1))); + orders.unpause(); + } + + function test_Revert_WhenPaused() public { + vm.prank(owner); + orders.pause(); + vm.expectRevert("Pausable: paused"); + orders.place(address(asset), 1 ether, 1); + vm.expectRevert("Pausable: paused"); + orders.cancel(1); + vm.expectRevert("Pausable: paused"); + orders.fill(1, address(100), 100); + } + + function testFuzz_NotPlacedOf(address someAsset, address buyer) public { + assertFalse(orders.isPlacedOrderOf(someAsset, buyer)); + vm.expectRevert(abi.encodeWithSelector(LSP7Orders.NotPlacedOf.selector, someAsset, buyer)); + orders.orderOf(someAsset, buyer); + } + + function testFuzz_NotPlaced(uint256 id) public { + assertFalse(orders.isPlacedOrder(id)); + vm.expectRevert(abi.encodeWithSelector(LSP7Orders.NotPlaced.selector, id)); + orders.getOrder(id); + } + + function testFuzz_Place(uint256 itemPrice, uint256 itemCount) public { + vm.assume(itemPrice < 100_000_000 ether); + vm.assume(itemCount > 0 && itemCount < 1_000_000); + + (UniversalProfile alice,) = deployProfile(); + + vm.deal(address(alice), itemPrice * itemCount); + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(1, address(asset), address(alice), itemPrice, itemCount); + orders.place{value: itemPrice * itemCount}(address(asset), itemPrice, itemCount); + + assertTrue(orders.isPlacedOrder(1)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + + LSP7Order memory order = orders.orderOf(address(asset), address(alice)); + assertEq(abi.encode(order), abi.encode(orders.getOrder(1))); + assertEq(order.id, 1); + assertEq(order.asset, address(asset)); + assertEq(order.buyer, address(alice)); + assertEq(order.itemPrice, itemPrice); + assertEq(order.itemCount, itemCount); + } + + function test_Revert_PlaceInvalidAmount() public { + (UniversalProfile alice,) = deployProfile(); + + vm.deal(address(alice), 1 ether); + vm.prank(address(alice)); + vm.expectRevert(abi.encodeWithSelector(LSP7Orders.InvalidAmount.selector, 2 ether, 1 ether)); + orders.place{value: 1 ether}(address(asset), 1 ether, 2); + } + + function test_Cancel() public { + (UniversalProfile alice,) = deployProfile(); + + vm.deal(address(alice), 1 ether); + vm.prank(address(alice)); + orders.place{value: 1 ether}(address(asset), 0.5 ether, 2); + + assertTrue(orders.isPlacedOrder(1)); + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + assertEq(address(alice).balance, 0 ether); + + vm.prank(address(alice)); + vm.expectEmit(); + emit Canceled(1, address(asset), address(alice), 0.5 ether, 2); + orders.cancel(1); + + assertFalse(orders.isPlacedOrder(1)); + assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); + assertEq(address(alice).balance, 1 ether); + } + + function testFuzz_Revert_CancelIfNotBuyer(address buyer) public { + (UniversalProfile alice,) = deployProfile(); + vm.assume(buyer != address(alice)); + + vm.deal(address(alice), 1 ether); + vm.prank(address(alice)); + orders.place{value: 1 ether}(address(asset), 0.5 ether, 2); + + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + + vm.prank(buyer); + vm.expectRevert(abi.encodeWithSelector(LSP7Orders.NotPlacedOf.selector, address(asset), buyer)); + orders.cancel(1); + + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + } + + function testFuzz_Fill(uint256 itemPrice, uint256 itemCount, uint256 fillCount) public { + vm.assume(itemPrice < 100_000_000 ether); + vm.assume(itemCount > 0 && itemCount < 1_000_000); + vm.assume(fillCount > 0 && fillCount < itemCount); + + (UniversalProfile alice,) = deployProfile(); + (UniversalProfile bob,) = deployProfile(); + + vm.deal(address(alice), itemPrice * itemCount); + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(1, address(asset), address(alice), itemPrice, itemCount); + orders.place{value: itemPrice * itemCount}(address(asset), itemPrice, itemCount); + + LSP7Order memory order = orders.orderOf(address(asset), address(alice)); + assertEq(order.id, 1); + assertEq(order.asset, address(asset)); + assertEq(order.buyer, address(alice)); + assertEq(order.itemPrice, itemPrice); + assertEq(order.itemCount, itemCount); + + address marketplace = address(100); + vm.prank(owner); + orders.grantRole(marketplace, MARKETPLACE_ROLE); + + assertEq(marketplace.balance, 0 ether); + + vm.prank(marketplace); + vm.expectEmit(); + emit Filled(1, address(asset), address(bob), address(alice), itemPrice, fillCount, itemCount); + orders.fill(1, address(bob), fillCount); + + assertEq(marketplace.balance, fillCount * itemPrice); + + assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); + order = orders.orderOf(address(asset), address(alice)); + assertEq(order.id, 1); + assertEq(order.asset, address(asset)); + assertEq(order.buyer, address(alice)); + assertEq(order.itemPrice, itemPrice); + assertEq(order.itemCount, itemCount - fillCount); + } + + function testFuzz_FillFully(uint256 itemPrice, uint256 itemCount) public { + vm.assume(itemPrice < 100_000_000 ether); + vm.assume(itemCount > 0 && itemCount < 1_000_000); + + (UniversalProfile alice,) = deployProfile(); + (UniversalProfile bob,) = deployProfile(); + + vm.deal(address(alice), itemPrice * itemCount); + vm.prank(address(alice)); + vm.expectEmit(); + emit Placed(1, address(asset), address(alice), itemPrice, itemCount); + orders.place{value: itemPrice * itemCount}(address(asset), itemPrice, itemCount); + + LSP7Order memory order = orders.orderOf(address(asset), address(alice)); + assertEq(order.id, 1); + assertEq(order.asset, address(asset)); + assertEq(order.buyer, address(alice)); + assertEq(order.itemPrice, itemPrice); + assertEq(order.itemCount, itemCount); + + address marketplace = address(100); + vm.prank(owner); + orders.grantRole(marketplace, MARKETPLACE_ROLE); + + assertEq(marketplace.balance, 0 ether); + + vm.prank(marketplace); + vm.expectEmit(); + emit Filled(1, address(asset), address(bob), address(alice), itemPrice, itemCount, itemCount); + orders.fill(1, address(bob), itemCount); + + assertEq(marketplace.balance, itemCount * itemPrice); + assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); + } +}