From 59a0e9ef5fcbdc013d26d000f09dc001085539b9 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Mon, 12 Feb 2024 11:26:14 +0100 Subject: [PATCH 1/8] LSP7 orders and upgraded marketplace --- .../abi/marketplace/lsp7/LSP7Marketplace.json | 128 +++- .../abi/marketplace/lsp7/LSP7Orders.json | 640 ++++++++++++++++++ src/marketplace/Participant.sol | 4 +- src/marketplace/lsp7/ILSP7Orders.sol | 58 ++ src/marketplace/lsp7/LSP7Marketplace.sol | 78 ++- src/marketplace/lsp7/LSP7Orders.sol | 107 +++ test/marketplace/lsp7/LSP7Marketplace.t.sol | 117 +++- tools/artifacts.sh | 1 + 8 files changed, 1074 insertions(+), 59 deletions(-) create mode 100644 artifacts/abi/marketplace/lsp7/LSP7Orders.json create mode 100644 src/marketplace/lsp7/ILSP7Orders.sol create mode 100644 src/marketplace/lsp7/LSP7Orders.sol diff --git a/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json b/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json index 116c939..7bf0ba9 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": [ @@ -520,6 +607,11 @@ "name": "offers_", "type": "address" }, + { + "internalType": "contract ILSP7Orders", + "name": "orders_", + "type": "address" + }, { "internalType": "contract IParticipant", "name": "participant_", @@ -557,6 +649,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "orders", + "outputs": [ + { + "internalType": "contract ILSP7Orders", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -649,6 +754,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract ILSP7Orders", + "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..f45506b --- /dev/null +++ b/artifacts/abi/marketplace/lsp7/LSP7Orders.json @@ -0,0 +1,640 @@ +[ + { + "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": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "Inactive", + "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": "secondsUntilExpiration", + "type": "uint256" + } + ], + "name": "InvalidDuration", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "InvalidItemCount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "NotPlaced", + "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": 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": 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": "itemCount", + "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": 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" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationTime", + "type": "uint256" + } + ], + "name": "Placed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "cancel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "seller", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + } + ], + "name": "fill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "getOrder", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "itemPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "itemCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expirationTime", + "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": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "isActiveOrder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" + } + ], + "name": "isPlacedOrder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "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" + }, + { + "internalType": "uint256", + "name": "secondsUntilExpiration", + "type": "uint256" + } + ], + "name": "place", + "outputs": [], + "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..fb5289a --- /dev/null +++ b/src/marketplace/lsp7/ILSP7Orders.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +// Order for LSP7 asset +struct LSP7Order { + uint256 id; + uint256 itemPrice; + uint256 itemCount; + uint256 expirationTime; +} + +interface ILSP7Orders { + /// an order was made for an asset + event Placed( + address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount, uint256 expirationTime + ); + /// a buyer canceled an order + event Canceled(address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); + /// an order was filled + event Filled( + address indexed asset, address indexed seller, address indexed buyer, uint256 itemPrice, uint256 itemCount + ); + + /// confirms an order has been placed by a buyer + /// @param asset asset address + /// @param buyer buyer + function isPlacedOrder(address asset, address buyer) external view returns (bool); + + /// confirms an order with asset address and buyer is active + /// @param asset asset address + /// @param buyer buyer + function isActiveOrder(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 getOrder(address asset, address buyer) 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 + /// @param secondsUntilExpiration time in seconds until offer is expired + function place(address asset, uint256 itemPrice, uint256 itemCount, uint256 secondsUntilExpiration) + external + payable; + + /// cancel an order by a buyer being a sender. + /// @param asset asset address + function cancel(address asset) external; + + /// fill an order. + /// @param asset asset address + /// @param seller seller + /// @param buyer buyer + /// @param itemCount number of items + function fill(address asset, address seller, address buyer, uint256 itemCount) external; +} diff --git a/src/marketplace/lsp7/LSP7Marketplace.sol b/src/marketplace/lsp7/LSP7Marketplace.sol index 06490e4..46c57a5 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(ILSP7Orders orders_) external onlyOwner { + orders = 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.encode(SALE_KIND_SPOT, listingId) + ); } function acceptOffer(uint256 listingId, address buyer) external whenNotPaused nonReentrant { @@ -64,16 +78,23 @@ 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.encode(SALE_KIND_OFFER, listingId) + ); } 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 +102,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); - } - if (feeAmount > 0) { - emit FeePaid(listingId, asset, itemCount, feeAmount); + revert Unpaid(seller, sellerAmount); } - 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..9751430 --- /dev/null +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +import {ILSP7DigitalAsset} from "@lukso/lsp-smart-contracts/contracts/LSP7DigitalAsset/ILSP7DigitalAsset.sol"; +import {Module} from "../common/Module.sol"; +import {ILSP7Orders, LSP7Order} from "./ILSP7Orders.sol"; + +contract LSP7Orders is ILSP7Orders, Module { + error Unpaid(address buyer, uint256 amount); + error NotPlaced(address asset, address buyer); + error Inactive(address asset, address buyer); + error InvalidItemCount(uint256 itemCount); + error InvalidDuration(uint256 secondsUntilExpiration); + 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 => LSP7Order order)) private _orders; + + constructor() { + _disableInitializers(); + } + + function initialize(address newOwner_) external initializer { + Module._initialize(newOwner_); + } + + function isPlacedOrder(address asset, address buyer) public view override returns (bool) { + LSP7Order memory order = _orders[asset][buyer]; + return order.itemCount > 0; + } + + function isActiveOrder(address asset, address buyer) public view override returns (bool) { + LSP7Order memory order = _orders[asset][buyer]; + return (order.itemCount > 0) && (order.expirationTime > block.timestamp); + } + + function getOrder(address asset, address buyer) public view override returns (LSP7Order memory) { + if (!isPlacedOrder(asset, buyer)) { + revert NotPlaced(asset, buyer); + } + return _orders[asset][buyer]; + } + + function place(address asset, uint256 itemPrice, uint256 itemCount, uint256 secondsUntilExpiration) + external + payable + override + whenNotPaused + nonReentrant + { + if (itemCount == 0) { + revert InvalidItemCount(itemCount); + } + if ((secondsUntilExpiration < 1 hours) || (secondsUntilExpiration > 28 days)) { + revert InvalidDuration(secondsUntilExpiration); + } + address buyer = msg.sender; + if (isPlacedOrder(asset, buyer)) { + revert AlreadyPlaced(asset, buyer); + } + uint256 totalValue = itemPrice * itemCount; + if (msg.value != totalValue) { + revert InvalidAmount(totalValue, msg.value); + } + uint256 expirationTime = block.timestamp + secondsUntilExpiration; + totalOrders += 1; + _orders[asset][buyer] = + LSP7Order({id: totalOrders, itemPrice: itemPrice, itemCount: itemCount, expirationTime: expirationTime}); + emit Placed(asset, buyer, itemPrice, itemCount, expirationTime); + } + + function cancel(address asset) external override whenNotPaused nonReentrant { + LSP7Order memory order = getOrder(asset, msg.sender); + uint256 totalValue = order.itemPrice * order.itemCount; + delete _orders[asset][msg.sender]; + (bool success,) = msg.sender.call{value: totalValue}(""); + if (!success) { + revert Unpaid(msg.sender, totalValue); + } + emit Canceled(asset, msg.sender, order.itemCount, order.itemPrice); + } + + function fill(address asset, address seller, address buyer, uint256 itemCount) + external + override + whenNotPaused + nonReentrant + onlyMarketplace + { + LSP7Order memory order = getOrder(asset, buyer); + if (!isActiveOrder(asset, buyer)) { + revert Inactive(asset, buyer); + } + if (itemCount > order.itemCount) { + revert InsufficientItemCount(order.itemCount, itemCount); + } + _orders[asset][buyer].itemCount -= itemCount; + uint256 totalValue = order.itemPrice * itemCount; + (bool success,) = msg.sender.call{value: totalValue}(""); + if (!success) { + revert Unpaid(msg.sender, totalValue); + } + emit Filled(asset, seller, buyer, order.itemPrice, itemCount); + } +} diff --git a/test/marketplace/lsp7/LSP7Marketplace.t.sol b/test/marketplace/lsp7/LSP7Marketplace.t.sol index 9690d11..d9edbd2 100644 --- a/test/marketplace/lsp7/LSP7Marketplace.t.sol +++ b/test/marketplace/lsp7/LSP7Marketplace.t.sol @@ -16,7 +16,8 @@ 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 {LSP7Marketplace} from "../../../src/marketplace/lsp7/LSP7Marketplace.sol"; +import {LSP7Orders} from "../../../src/marketplace/lsp7/LSP7Orders.sol"; +import {LSP7Marketplace, SALE_KIND_SPOT, SALE_KIND_OFFER} from "../../../src/marketplace/lsp7/LSP7Marketplace.sol"; import {Participant, GENESIS_DISCOUNT} from "../../../src/marketplace/Participant.sol"; import {deployProfile} from "../../utils/profile.sol"; import {LSP7DigitalAssetMock} from "./LSP7DigitalAssetMock.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, + abi.encode(SALE_KIND_SPOT, uint256(1)) + ); 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, + abi.encode(SALE_KIND_SPOT, uint256(1)) + ); 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, + abi.encode(SALE_KIND_SPOT, uint256(1)) + ); 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, + abi.encode(SALE_KIND_SPOT, uint256(1)) + ); 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, + abi.encode(SALE_KIND_SPOT, uint256(1)) + ); 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, + abi.encode(SALE_KIND_OFFER, uint256(1)) + ); marketplace.acceptOffer(1, address(bob)); assertFalse(listings.isListed(1)); diff --git a/tools/artifacts.sh b/tools/artifacts.sh index 7626d32..4c58786 100755 --- a/tools/artifacts.sh +++ b/tools/artifacts.sh @@ -106,6 +106,7 @@ exportAbi "marketplace/Participant" exportAbi "marketplace/lsp7/LSP7Listings" exportAbi "marketplace/lsp7/LSP7Offers" +exportAbi "marketplace/lsp7/LSP7Orders" exportAbi "marketplace/lsp7/LSP7Marketplace" exportAbi "marketplace/lsp8/LSP8Listings" From 68eae6181785ec346121f2779c41fddfe69c4c33 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Mon, 12 Feb 2024 12:54:20 +0100 Subject: [PATCH 2/8] correct data encoding packing --- src/marketplace/lsp7/LSP7Marketplace.sol | 4 ++-- test/marketplace/lsp7/LSP7Marketplace.t.sol | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/marketplace/lsp7/LSP7Marketplace.sol b/src/marketplace/lsp7/LSP7Marketplace.sol index 46c57a5..dd29861 100644 --- a/src/marketplace/lsp7/LSP7Marketplace.sol +++ b/src/marketplace/lsp7/LSP7Marketplace.sol @@ -66,7 +66,7 @@ contract LSP7Marketplace is Base { } listings.deduct(listingId, itemCount); _executeSale( - listing.asset, itemCount, listing.owner, recipient, msg.value, abi.encode(SALE_KIND_SPOT, listingId) + listing.asset, itemCount, listing.owner, recipient, msg.value, abi.encodePacked(SALE_KIND_SPOT, listingId) ); } @@ -84,7 +84,7 @@ contract LSP7Marketplace is Base { listing.owner, buyer, offer.totalPrice, - abi.encode(SALE_KIND_OFFER, listingId) + abi.encodePacked(SALE_KIND_OFFER, listingId) ); } diff --git a/test/marketplace/lsp7/LSP7Marketplace.t.sol b/test/marketplace/lsp7/LSP7Marketplace.t.sol index d9edbd2..81cc721 100644 --- a/test/marketplace/lsp7/LSP7Marketplace.t.sol +++ b/test/marketplace/lsp7/LSP7Marketplace.t.sol @@ -17,7 +17,7 @@ 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, SALE_KIND_SPOT, SALE_KIND_OFFER} from "../../../src/marketplace/lsp7/LSP7Marketplace.sol"; +import {LSP7Marketplace} from "../../../src/marketplace/lsp7/LSP7Marketplace.sol"; import {Participant, GENESIS_DISCOUNT} from "../../../src/marketplace/Participant.sol"; import {deployProfile} from "../../utils/profile.sol"; import {LSP7DigitalAssetMock} from "./LSP7DigitalAssetMock.sol"; @@ -204,7 +204,7 @@ contract LSP7MarketplaceTest is Test { totalPrice, 0, 0, - abi.encode(SALE_KIND_SPOT, uint256(1)) + hex"000000000000000000000000000000000000000000000000000000000000000001" ); marketplace.buy{value: totalPrice}(1, itemCount, address(bob)); @@ -246,7 +246,7 @@ contract LSP7MarketplaceTest is Test { totalPrice, feeAmount, 0, - abi.encode(SALE_KIND_SPOT, uint256(1)) + hex"000000000000000000000000000000000000000000000000000000000000000001" ); marketplace.buy{value: totalPrice}(1, itemCount, address(bob)); @@ -292,7 +292,7 @@ contract LSP7MarketplaceTest is Test { totalPrice, feeAmount - discountFeeAmount, 0, - abi.encode(SALE_KIND_SPOT, uint256(1)) + hex"000000000000000000000000000000000000000000000000000000000000000001" ); marketplace.buy{value: totalPrice}(1, 10, address(bob)); @@ -348,7 +348,7 @@ contract LSP7MarketplaceTest is Test { totalPrice, 0, royaltiesAmount0 + royaltiesAmount1, - abi.encode(SALE_KIND_SPOT, uint256(1)) + hex"000000000000000000000000000000000000000000000000000000000000000001" ); if (royaltiesAmount0 > 0) { vm.expectEmit(address(marketplace)); @@ -394,7 +394,7 @@ contract LSP7MarketplaceTest is Test { 10 ether, 1 ether, 0, - abi.encode(SALE_KIND_SPOT, uint256(1)) + hex"000000000000000000000000000000000000000000000000000000000000000001" ); marketplace.buy{value: 10 ether}(1, 10, address(bob)); @@ -465,7 +465,7 @@ contract LSP7MarketplaceTest is Test { totalPrice, 0, 0, - abi.encode(SALE_KIND_OFFER, uint256(1)) + hex"010000000000000000000000000000000000000000000000000000000000000001" ); marketplace.acceptOffer(1, address(bob)); From b40c5661de0b55299594fc86b4127f3530a27e62 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Mon, 12 Feb 2024 16:24:22 +0100 Subject: [PATCH 3/8] LSP7 orders --- README.md | 2 + .../abi/marketplace/lsp7/LSP7Marketplace.json | 2 +- .../abi/marketplace/lsp7/LSP7Orders.json | 135 ++++++---- .../marketplace/lsp7/LSP7Marketplace.s.sol | 9 +- scripts/marketplace/lsp7/LSP7Orders.s.sol | 39 +++ src/marketplace/lsp7/ILSP7Orders.sol | 38 +-- src/marketplace/lsp7/LSP7Marketplace.sol | 4 +- src/marketplace/lsp7/LSP7Orders.sol | 72 ++--- test/marketplace/lsp7/LSP7Orders.t.sol | 253 ++++++++++++++++++ 9 files changed, 449 insertions(+), 105 deletions(-) create mode 100644 scripts/marketplace/lsp7/LSP7Orders.s.sol create mode 100644 test/marketplace/lsp7/LSP7Orders.t.sol diff --git a/README.md b/README.md index b130c79..643f678 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 7bf0ba9..d00b138 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json @@ -757,7 +757,7 @@ { "inputs": [ { - "internalType": "contract ILSP7Orders", + "internalType": "address", "name": "orders_", "type": "address" } diff --git a/artifacts/abi/marketplace/lsp7/LSP7Orders.json b/artifacts/abi/marketplace/lsp7/LSP7Orders.json index f45506b..65245b3 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Orders.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Orders.json @@ -36,22 +36,6 @@ "name": "IllegalAccess", "type": "error" }, - { - "inputs": [ - { - "internalType": "address", - "name": "asset", - "type": "address" - }, - { - "internalType": "address", - "name": "buyer", - "type": "address" - } - ], - "name": "Inactive", - "type": "error" - }, { "inputs": [ { @@ -99,22 +83,22 @@ "inputs": [ { "internalType": "uint256", - "name": "secondsUntilExpiration", + "name": "itemCount", "type": "uint256" } ], - "name": "InvalidDuration", + "name": "InvalidItemCount", "type": "error" }, { "inputs": [ { "internalType": "uint256", - "name": "itemCount", + "name": "id", "type": "uint256" } ], - "name": "InvalidItemCount", + "name": "NotPlaced", "type": "error" }, { @@ -130,7 +114,7 @@ "type": "address" } ], - "name": "NotPlaced", + "name": "NotPlacedOf", "type": "error" }, { @@ -168,6 +152,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, { "indexed": true, "internalType": "address", @@ -199,6 +189,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, { "indexed": true, "internalType": "address", @@ -281,6 +277,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, { "indexed": true, "internalType": "address", @@ -304,12 +306,6 @@ "internalType": "uint256", "name": "itemCount", "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "expirationTime", - "type": "uint256" } ], "name": "Placed", @@ -372,14 +368,9 @@ { "inputs": [ { - "internalType": "address", - "name": "asset", - "type": "address" - }, - { - "internalType": "address", - "name": "buyer", - "type": "address" + "internalType": "uint256", + "name": "id", + "type": "uint256" } ], "name": "getOrder", @@ -392,18 +383,23 @@ "type": "uint256" }, { - "internalType": "uint256", - "name": "itemPrice", - "type": "uint256" + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "address", + "name": "buyer", + "type": "address" }, { "internalType": "uint256", - "name": "itemCount", + "name": "itemPrice", "type": "uint256" }, { "internalType": "uint256", - "name": "expirationTime", + "name": "itemCount", "type": "uint256" } ], @@ -470,6 +466,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "isPlacedOrder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -483,7 +498,7 @@ "type": "address" } ], - "name": "isActiveOrder", + "name": "isPlacedOrderOf", "outputs": [ { "internalType": "bool", @@ -507,12 +522,39 @@ "type": "address" } ], - "name": "isPlacedOrder", + "name": "orderOf", "outputs": [ { - "internalType": "bool", + "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": "bool" + "type": "tuple" } ], "stateMutability": "view", @@ -567,11 +609,6 @@ "internalType": "uint256", "name": "itemCount", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "secondsUntilExpiration", - "type": "uint256" } ], "name": "place", diff --git a/scripts/marketplace/lsp7/LSP7Marketplace.s.sol b/scripts/marketplace/lsp7/LSP7Marketplace.s.sol index 5a09dd2..498aec6 100644 --- a/scripts/marketplace/lsp7/LSP7Marketplace.s.sol +++ b/scripts/marketplace/lsp7/LSP7Marketplace.s.sol @@ -22,6 +22,7 @@ contract Deploy is Script { address treasury = vm.envAddress("TREASURY_ADDRESS"); address listings = vm.envAddress("CONTRACT_LSP7_LISTINGS_ADDRESS"); address offers = vm.envAddress("CONTRACT_LSP7_OFFERS_ADDRESS"); + address orders = vm.envAddress("CONTRACT_LSP7_ORDERS_ADDRESS"); address participant = vm.envAddress("CONTRACT_PARTICIPANT_ADDRESS"); address proxy = vm.envOr("CONTRACT_LSP7_MARKETPLACE_ADDRESS", address(0)); @@ -35,7 +36,7 @@ contract Deploy is Script { new TransparentUpgradeableProxy( address(marketplace), admin, - abi.encodeWithSelector(LSP7Marketplace.initialize.selector, owner, treasury, listings, offers, participant) + abi.encodeWithSelector(LSP7Marketplace.initialize.selector, owner, treasury, listings, offers, orders, participant) ) ); console.log(string.concat("LSP7Marketplace: deploy ", Strings.toHexString(address(proxy)))); @@ -71,8 +72,14 @@ contract Configure is Script { IParticipant participant = IParticipant(vm.envAddress("CONTRACT_PARTICIPANT_ADDRESS")); Module listings = Module(vm.envAddress("CONTRACT_LSP7_LISTINGS_ADDRESS")); Module offers = Module(vm.envAddress("CONTRACT_LSP7_OFFERS_ADDRESS")); + Module orders = Module(vm.envAddress("CONTRACT_LSP7_ORDERS_ADDRESS")); LSP7Marketplace marketplace = LSP7Marketplace(payable(vm.envAddress("CONTRACT_LSP7_MARKETPLACE_ADDRESS"))); + if (address(marketplace.orders()) != address(orders)) { + vm.broadcast(owner); + marketplace.setOrders(address(orders)); + } + if (marketplace.feePoints() != FEE_POINTS) { vm.broadcast(owner); marketplace.setFeePoints(FEE_POINTS); diff --git a/scripts/marketplace/lsp7/LSP7Orders.s.sol b/scripts/marketplace/lsp7/LSP7Orders.s.sol new file mode 100644 index 0000000..e0f1223 --- /dev/null +++ b/scripts/marketplace/lsp7/LSP7Orders.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.22; + +import "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {LSP7Orders} from "../../../src/marketplace/lsp7/LSP7Orders.sol"; + +contract Deploy is Script { + function run() external { + address admin = vm.envAddress("ADMIN_ADDRESS"); + address owner = vm.envAddress("OWNER_ADDRESS"); + + address proxy = vm.envOr("CONTRACT_LSP7_ORDERS_ADDRESS", address(0)); + + vm.broadcast(admin); + LSP7Orders offers = new LSP7Orders(); + + if (proxy == address(0)) { + vm.broadcast(admin); + proxy = address( + new TransparentUpgradeableProxy( + address(offers), + admin, + abi.encodeWithSelector(LSP7Orders.initialize.selector, owner) + ) + ); + console.log(string.concat("LSP7Orders: deploy ", Strings.toHexString(address(proxy)))); + } else { + vm.broadcast(admin); + ITransparentUpgradeableProxy(proxy).upgradeTo(address(offers)); + console.log(string.concat("LSP7Orders: upgrade ", Strings.toHexString(address(proxy)))); + } + } +} diff --git a/src/marketplace/lsp7/ILSP7Orders.sol b/src/marketplace/lsp7/ILSP7Orders.sol index fb5289a..bebd37e 100644 --- a/src/marketplace/lsp7/ILSP7Orders.sol +++ b/src/marketplace/lsp7/ILSP7Orders.sol @@ -4,46 +4,50 @@ pragma solidity =0.8.22; // Order for LSP7 asset struct LSP7Order { uint256 id; + address asset; + address buyer; uint256 itemPrice; uint256 itemCount; - uint256 expirationTime; } interface ILSP7Orders { /// an order was made for an asset - event Placed( - address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount, uint256 expirationTime - ); + event Placed(uint256 id, address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); /// a buyer canceled an order - event Canceled(address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); + event Canceled(uint256 id, address indexed asset, address indexed buyer, uint256 itemPrice, uint256 itemCount); /// an order was filled event Filled( - address indexed asset, address indexed seller, address indexed buyer, uint256 itemPrice, uint256 itemCount + uint256 id, + address indexed asset, + address indexed seller, + address indexed buyer, + uint256 itemPrice, + uint256 itemCount ); /// confirms an order has been placed by a buyer /// @param asset asset address /// @param buyer buyer - function isPlacedOrder(address asset, address buyer) external view returns (bool); - - /// confirms an order with asset address and buyer is active - /// @param asset asset address - /// @param buyer buyer - function isActiveOrder(address asset, address buyer) external view returns (bool); + 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 getOrder(address asset, address buyer) external view returns (LSP7Order memory); + 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 - /// @param secondsUntilExpiration time in seconds until offer is expired - function place(address asset, uint256 itemPrice, uint256 itemCount, uint256 secondsUntilExpiration) - external - payable; + function place(address asset, uint256 itemPrice, uint256 itemCount) external payable; /// cancel an order by a buyer being a sender. /// @param asset asset address diff --git a/src/marketplace/lsp7/LSP7Marketplace.sol b/src/marketplace/lsp7/LSP7Marketplace.sol index dd29861..6cd72b2 100644 --- a/src/marketplace/lsp7/LSP7Marketplace.sol +++ b/src/marketplace/lsp7/LSP7Marketplace.sol @@ -55,8 +55,8 @@ contract LSP7Marketplace is Base { orders = orders_; } - function setOrders(ILSP7Orders orders_) external onlyOwner { - orders = orders_; + function setOrders(address orders_) external onlyOwner { + orders = ILSP7Orders(orders_); } function buy(uint256 listingId, uint256 itemCount, address recipient) external payable whenNotPaused nonReentrant { diff --git a/src/marketplace/lsp7/LSP7Orders.sol b/src/marketplace/lsp7/LSP7Orders.sol index 9751430..2711ed5 100644 --- a/src/marketplace/lsp7/LSP7Orders.sol +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -1,22 +1,21 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity =0.8.22; -import {ILSP7DigitalAsset} from "@lukso/lsp-smart-contracts/contracts/LSP7DigitalAsset/ILSP7DigitalAsset.sol"; import {Module} from "../common/Module.sol"; import {ILSP7Orders, LSP7Order} from "./ILSP7Orders.sol"; contract LSP7Orders is ILSP7Orders, Module { error Unpaid(address buyer, uint256 amount); - error NotPlaced(address asset, address buyer); - error Inactive(address asset, address buyer); + error NotPlacedOf(address asset, address buyer); + error NotPlaced(uint256 id); error InvalidItemCount(uint256 itemCount); - error InvalidDuration(uint256 secondsUntilExpiration); 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 => LSP7Order order)) private _orders; + mapping(address asset => mapping(address buyer => uint256)) private _orderIds; + mapping(uint256 id => LSP7Order) private _orders; constructor() { _disableInitializers(); @@ -26,24 +25,30 @@ contract LSP7Orders is ILSP7Orders, Module { Module._initialize(newOwner_); } - function isPlacedOrder(address asset, address buyer) public view override returns (bool) { - LSP7Order memory order = _orders[asset][buyer]; - return order.itemCount > 0; + function isPlacedOrderOf(address asset, address buyer) public view override returns (bool) { + return isPlacedOrder(_orderIds[asset][buyer]); } - function isActiveOrder(address asset, address buyer) public view override returns (bool) { - LSP7Order memory order = _orders[asset][buyer]; - return (order.itemCount > 0) && (order.expirationTime > block.timestamp); + 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(address asset, address buyer) public view override returns (LSP7Order memory) { - if (!isPlacedOrder(asset, buyer)) { - revert NotPlaced(asset, buyer); + function getOrder(uint256 id) public view override returns (LSP7Order memory) { + if (!isPlacedOrder(id)) { + revert NotPlaced(id); } - return _orders[asset][buyer]; + return _orders[id]; } - function place(address asset, uint256 itemPrice, uint256 itemCount, uint256 secondsUntilExpiration) + function place(address asset, uint256 itemPrice, uint256 itemCount) external payable override @@ -53,33 +58,33 @@ contract LSP7Orders is ILSP7Orders, Module { if (itemCount == 0) { revert InvalidItemCount(itemCount); } - if ((secondsUntilExpiration < 1 hours) || (secondsUntilExpiration > 28 days)) { - revert InvalidDuration(secondsUntilExpiration); - } address buyer = msg.sender; - if (isPlacedOrder(asset, buyer)) { + if (isPlacedOrderOf(asset, buyer)) { revert AlreadyPlaced(asset, buyer); } uint256 totalValue = itemPrice * itemCount; if (msg.value != totalValue) { revert InvalidAmount(totalValue, msg.value); } - uint256 expirationTime = block.timestamp + secondsUntilExpiration; totalOrders += 1; - _orders[asset][buyer] = - LSP7Order({id: totalOrders, itemPrice: itemPrice, itemCount: itemCount, expirationTime: expirationTime}); - emit Placed(asset, buyer, itemPrice, itemCount, expirationTime); + 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); } function cancel(address asset) external override whenNotPaused nonReentrant { - LSP7Order memory order = getOrder(asset, msg.sender); + address buyer = msg.sender; + LSP7Order memory order = orderOf(asset, buyer); + delete _orders[order.id]; + delete _orderIds[asset][buyer]; uint256 totalValue = order.itemPrice * order.itemCount; - delete _orders[asset][msg.sender]; - (bool success,) = msg.sender.call{value: totalValue}(""); + (bool success,) = buyer.call{value: totalValue}(""); if (!success) { - revert Unpaid(msg.sender, totalValue); + revert Unpaid(buyer, totalValue); } - emit Canceled(asset, msg.sender, order.itemCount, order.itemPrice); + emit Canceled(order.id, asset, buyer, order.itemPrice, order.itemCount); } function fill(address asset, address seller, address buyer, uint256 itemCount) @@ -89,19 +94,16 @@ contract LSP7Orders is ILSP7Orders, Module { nonReentrant onlyMarketplace { - LSP7Order memory order = getOrder(asset, buyer); - if (!isActiveOrder(asset, buyer)) { - revert Inactive(asset, buyer); - } + LSP7Order memory order = orderOf(asset, buyer); if (itemCount > order.itemCount) { revert InsufficientItemCount(order.itemCount, itemCount); } - _orders[asset][buyer].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(asset, seller, buyer, order.itemPrice, itemCount); + emit Filled(order.id, asset, seller, buyer, order.itemPrice, itemCount); } } diff --git a/test/marketplace/lsp7/LSP7Orders.t.sol b/test/marketplace/lsp7/LSP7Orders.t.sol new file mode 100644 index 0000000..7f899e8 --- /dev/null +++ b/test/marketplace/lsp7/LSP7Orders.t.sol @@ -0,0 +1,253 @@ +// 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 itemCount + ); + + 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(address(asset)); + vm.expectRevert("Pausable: paused"); + orders.fill(address(asset), address(100), address(101), 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(address(asset)); + + 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(address(asset)); + + 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); + orders.fill(address(asset), address(bob), address(alice), 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); + orders.fill(address(asset), address(bob), address(alice), itemCount); + + assertEq(marketplace.balance, itemCount * itemPrice); + assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); + } +} From be88b15c12ce1d0de0596b60ee7c80f797322419 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Mon, 12 Feb 2024 16:46:05 +0100 Subject: [PATCH 4/8] Enable lsp7 orders on marketplace --- .../abi/marketplace/lsp7/LSP7Marketplace.json | 18 ++++++++ .../abi/marketplace/lsp7/LSP7Orders.json | 8 +++- .../marketplace/lsp7/LSP7Marketplace.s.sol | 5 +++ src/marketplace/lsp7/ILSP7Orders.sol | 3 +- src/marketplace/lsp7/LSP7Marketplace.sol | 14 +++++++ src/marketplace/lsp7/LSP7Orders.sol | 2 + test/marketplace/lsp7/LSP7Marketplace.t.sol | 42 +++++++++++++++++++ 7 files changed, 90 insertions(+), 2 deletions(-) diff --git a/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json b/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json index d00b138..b2ae86f 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Marketplace.json @@ -585,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": [ { diff --git a/artifacts/abi/marketplace/lsp7/LSP7Orders.json b/artifacts/abi/marketplace/lsp7/LSP7Orders.json index 65245b3..d678b5d 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Orders.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Orders.json @@ -612,7 +612,13 @@ } ], "name": "place", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "payable", "type": "function" }, diff --git a/scripts/marketplace/lsp7/LSP7Marketplace.s.sol b/scripts/marketplace/lsp7/LSP7Marketplace.s.sol index 498aec6..ce8e51b 100644 --- a/scripts/marketplace/lsp7/LSP7Marketplace.s.sol +++ b/scripts/marketplace/lsp7/LSP7Marketplace.s.sol @@ -100,6 +100,11 @@ contract Configure is Script { offers.grantRole(address(marketplace), MARKETPLACE_ROLE); } + if (!orders.hasRole(address(marketplace), MARKETPLACE_ROLE)) { + vm.broadcast(owner); + orders.grantRole(address(marketplace), MARKETPLACE_ROLE); + } + if (marketplace.beneficiary() != treasury) { vm.broadcast(owner); marketplace.setBeneficiary(treasury); diff --git a/src/marketplace/lsp7/ILSP7Orders.sol b/src/marketplace/lsp7/ILSP7Orders.sol index bebd37e..c5e0164 100644 --- a/src/marketplace/lsp7/ILSP7Orders.sol +++ b/src/marketplace/lsp7/ILSP7Orders.sol @@ -47,7 +47,8 @@ interface ILSP7Orders { /// @param asset asset address /// @param itemPrice item price /// @param itemCount number of items - function place(address asset, uint256 itemPrice, uint256 itemCount) external payable; + /// @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 asset asset address diff --git a/src/marketplace/lsp7/LSP7Marketplace.sol b/src/marketplace/lsp7/LSP7Marketplace.sol index 6cd72b2..8bfbf72 100644 --- a/src/marketplace/lsp7/LSP7Marketplace.sol +++ b/src/marketplace/lsp7/LSP7Marketplace.sol @@ -88,6 +88,20 @@ contract LSP7Marketplace is Base { ); } + function fillOrder(uint256 orderId, uint256 itemCount) external whenNotPaused nonReentrant { + address seller = msg.sender; + LSP7Order memory order = orders.getOrder(orderId); + orders.fill(order.asset, seller, order.buyer, itemCount); + _executeSale( + order.asset, + itemCount, + seller, + order.buyer, + order.itemPrice * itemCount, + abi.encodePacked(SALE_KIND_ORDER, order.id) + ); + } + function _executeSale( address asset, uint256 itemCount, diff --git a/src/marketplace/lsp7/LSP7Orders.sol b/src/marketplace/lsp7/LSP7Orders.sol index 2711ed5..dd69034 100644 --- a/src/marketplace/lsp7/LSP7Orders.sol +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -54,6 +54,7 @@ contract LSP7Orders is ILSP7Orders, Module { override whenNotPaused nonReentrant + returns (uint256) { if (itemCount == 0) { revert InvalidItemCount(itemCount); @@ -72,6 +73,7 @@ contract LSP7Orders is ILSP7Orders, Module { _orders[orderId] = LSP7Order({id: orderId, asset: asset, buyer: buyer, itemPrice: itemPrice, itemCount: itemCount}); emit Placed(orderId, asset, buyer, itemPrice, itemCount); + return orderId; } function cancel(address asset) external override whenNotPaused nonReentrant { diff --git a/test/marketplace/lsp7/LSP7Marketplace.t.sol b/test/marketplace/lsp7/LSP7Marketplace.t.sol index 81cc721..d8f6c4b 100644 --- a/test/marketplace/lsp7/LSP7Marketplace.t.sol +++ b/test/marketplace/lsp7/LSP7Marketplace.t.sol @@ -475,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); + } } From 7ace3802fa3a1bd04ecda0127a6f4f2089c5262a Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Tue, 13 Feb 2024 10:05:07 +0100 Subject: [PATCH 5/8] Update lsp7 fill event --- artifacts/abi/marketplace/lsp7/LSP7Orders.json | 8 +++++++- src/marketplace/lsp7/ILSP7Orders.sol | 3 ++- src/marketplace/lsp7/LSP7Orders.sol | 2 +- test/marketplace/lsp7/LSP7Orders.t.sol | 7 ++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/artifacts/abi/marketplace/lsp7/LSP7Orders.json b/artifacts/abi/marketplace/lsp7/LSP7Orders.json index d678b5d..3b818e0 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Orders.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Orders.json @@ -222,7 +222,13 @@ { "indexed": false, "internalType": "uint256", - "name": "itemCount", + "name": "fillCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalCount", "type": "uint256" } ], diff --git a/src/marketplace/lsp7/ILSP7Orders.sol b/src/marketplace/lsp7/ILSP7Orders.sol index c5e0164..a6dd1d2 100644 --- a/src/marketplace/lsp7/ILSP7Orders.sol +++ b/src/marketplace/lsp7/ILSP7Orders.sol @@ -22,7 +22,8 @@ interface ILSP7Orders { address indexed seller, address indexed buyer, uint256 itemPrice, - uint256 itemCount + uint256 fillCount, + uint256 totalCount ); /// confirms an order has been placed by a buyer diff --git a/src/marketplace/lsp7/LSP7Orders.sol b/src/marketplace/lsp7/LSP7Orders.sol index dd69034..6b52829 100644 --- a/src/marketplace/lsp7/LSP7Orders.sol +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -106,6 +106,6 @@ contract LSP7Orders is ILSP7Orders, Module { if (!success) { revert Unpaid(msg.sender, totalValue); } - emit Filled(order.id, asset, seller, buyer, order.itemPrice, itemCount); + emit Filled(order.id, asset, seller, buyer, order.itemPrice, itemCount, order.itemCount); } } diff --git a/test/marketplace/lsp7/LSP7Orders.t.sol b/test/marketplace/lsp7/LSP7Orders.t.sol index 7f899e8..5c8a9e9 100644 --- a/test/marketplace/lsp7/LSP7Orders.t.sol +++ b/test/marketplace/lsp7/LSP7Orders.t.sol @@ -24,7 +24,8 @@ contract LSP7OrdersTest is Test { address indexed seller, address indexed buyer, uint256 itemPrice, - uint256 itemCount + uint256 fillCount, + uint256 totalCount ); LSP7Orders orders; @@ -202,7 +203,7 @@ contract LSP7OrdersTest is Test { vm.prank(marketplace); vm.expectEmit(); - emit Filled(1, address(asset), address(bob), address(alice), itemPrice, fillCount); + emit Filled(1, address(asset), address(bob), address(alice), itemPrice, fillCount, itemCount); orders.fill(address(asset), address(bob), address(alice), fillCount); assertEq(marketplace.balance, fillCount * itemPrice); @@ -244,7 +245,7 @@ contract LSP7OrdersTest is Test { vm.prank(marketplace); vm.expectEmit(); - emit Filled(1, address(asset), address(bob), address(alice), itemPrice, itemCount); + emit Filled(1, address(asset), address(bob), address(alice), itemPrice, itemCount, itemCount); orders.fill(address(asset), address(bob), address(alice), itemCount); assertEq(marketplace.balance, itemCount * itemPrice); From ad9a56ac77c91637842ba4a05ee8491d419de8d4 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Tue, 13 Feb 2024 10:23:00 +0100 Subject: [PATCH 6/8] Add claiming for pro nft --- .../CollectorIdentifiableDigitalAsset.s.sol | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/assets/lsp8/CollectorIdentifiableDigitalAsset.s.sol b/scripts/assets/lsp8/CollectorIdentifiableDigitalAsset.s.sol index 966d008..80c40d3 100644 --- a/scripts/assets/lsp8/CollectorIdentifiableDigitalAsset.s.sol +++ b/scripts/assets/lsp8/CollectorIdentifiableDigitalAsset.s.sol @@ -22,6 +22,30 @@ contract Deploy is Script { } } +contract Claim is Script { + function run() external { + address profileController = vm.envAddress("PROFILE_CONTROLLER_ADDRESS"); + UniversalProfile profile = UniversalProfile(payable(vm.envAddress("PROFILE_ADDRESS"))); + CollectorIdentifiableDigitalAsset asset = + CollectorIdentifiableDigitalAsset(payable(vm.envAddress("CONTRACT_COLLECTOR_DIGITAL_ASSET_ADDRESS"))); + + if (address(asset).balance > 0) { + console.log( + string.concat("CollectorIdentifiableDigitalAsset: withdraw ", Strings.toString(address(asset).balance)), + "wei to", + Strings.toHexString(asset.beneficiary()) + ); + vm.broadcast(profileController); + profile.execute( + OPERATION_0_CALL, + address(asset), + 0, + abi.encodeWithSelector(asset.withdraw.selector, address(asset).balance) + ); + } + } +} + contract Configure is Script { bytes32 private constant _LSP8_TOKEN_METADATA_BASE_URI_KEY = 0x1a7628600c3bac7101f53697f48df381ddc36b9015e7d7c9c5633d1252aa2843; From 0c5fd64d9c19e90b949843f7973a41ab34403829 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Tue, 13 Feb 2024 13:08:10 +0100 Subject: [PATCH 7/8] Simplify lsp7 orders based on order id --- .../abi/marketplace/lsp7/LSP7Orders.json | 17 +++++--------- src/marketplace/lsp7/ILSP7Orders.sol | 9 ++++---- src/marketplace/lsp7/LSP7Marketplace.sol | 2 +- src/marketplace/lsp7/LSP7Orders.sol | 23 +++++++++++-------- test/marketplace/lsp7/LSP7Orders.t.sol | 12 +++++----- 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/artifacts/abi/marketplace/lsp7/LSP7Orders.json b/artifacts/abi/marketplace/lsp7/LSP7Orders.json index 3b818e0..6cfb4c2 100644 --- a/artifacts/abi/marketplace/lsp7/LSP7Orders.json +++ b/artifacts/abi/marketplace/lsp7/LSP7Orders.json @@ -333,9 +333,9 @@ { "inputs": [ { - "internalType": "address", - "name": "asset", - "type": "address" + "internalType": "uint256", + "name": "id", + "type": "uint256" } ], "name": "cancel", @@ -346,20 +346,15 @@ { "inputs": [ { - "internalType": "address", - "name": "asset", - "type": "address" + "internalType": "uint256", + "name": "id", + "type": "uint256" }, { "internalType": "address", "name": "seller", "type": "address" }, - { - "internalType": "address", - "name": "buyer", - "type": "address" - }, { "internalType": "uint256", "name": "itemCount", diff --git a/src/marketplace/lsp7/ILSP7Orders.sol b/src/marketplace/lsp7/ILSP7Orders.sol index a6dd1d2..724d3ea 100644 --- a/src/marketplace/lsp7/ILSP7Orders.sol +++ b/src/marketplace/lsp7/ILSP7Orders.sol @@ -52,13 +52,12 @@ interface ILSP7Orders { function place(address asset, uint256 itemPrice, uint256 itemCount) external payable returns (uint256); /// cancel an order by a buyer being a sender. - /// @param asset asset address - function cancel(address asset) external; + /// @param id order id + function cancel(uint256 id) external; /// fill an order. - /// @param asset asset address + /// @param id order id /// @param seller seller - /// @param buyer buyer /// @param itemCount number of items - function fill(address asset, address seller, address buyer, uint256 itemCount) external; + function fill(uint256 id, address seller, uint256 itemCount) external; } diff --git a/src/marketplace/lsp7/LSP7Marketplace.sol b/src/marketplace/lsp7/LSP7Marketplace.sol index 8bfbf72..99b1679 100644 --- a/src/marketplace/lsp7/LSP7Marketplace.sol +++ b/src/marketplace/lsp7/LSP7Marketplace.sol @@ -91,7 +91,7 @@ contract LSP7Marketplace is Base { function fillOrder(uint256 orderId, uint256 itemCount) external whenNotPaused nonReentrant { address seller = msg.sender; LSP7Order memory order = orders.getOrder(orderId); - orders.fill(order.asset, seller, order.buyer, itemCount); + orders.fill(orderId, seller, itemCount); _executeSale( order.asset, itemCount, diff --git a/src/marketplace/lsp7/LSP7Orders.sol b/src/marketplace/lsp7/LSP7Orders.sol index 6b52829..7c086f6 100644 --- a/src/marketplace/lsp7/LSP7Orders.sol +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -76,27 +76,30 @@ contract LSP7Orders is ILSP7Orders, Module { return orderId; } - function cancel(address asset) external override whenNotPaused nonReentrant { + function cancel(uint256 id) external override whenNotPaused nonReentrant { address buyer = msg.sender; - LSP7Order memory order = orderOf(asset, buyer); - delete _orders[order.id]; - delete _orderIds[asset][buyer]; + 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,) = buyer.call{value: totalValue}(""); + (bool success,) = order.buyer.call{value: totalValue}(""); if (!success) { - revert Unpaid(buyer, totalValue); + revert Unpaid(order.buyer, totalValue); } - emit Canceled(order.id, asset, buyer, order.itemPrice, order.itemCount); + emit Canceled(order.id, order.asset, order.buyer, order.itemPrice, order.itemCount); } - function fill(address asset, address seller, address buyer, uint256 itemCount) + function fill(uint256 id, address seller, uint256 itemCount) external override whenNotPaused nonReentrant onlyMarketplace { - LSP7Order memory order = orderOf(asset, buyer); + LSP7Order memory order = getOrder(id); if (itemCount > order.itemCount) { revert InsufficientItemCount(order.itemCount, itemCount); } @@ -106,6 +109,6 @@ contract LSP7Orders is ILSP7Orders, Module { if (!success) { revert Unpaid(msg.sender, totalValue); } - emit Filled(order.id, asset, seller, buyer, order.itemPrice, itemCount, order.itemCount); + emit Filled(order.id, order.asset, seller, order.buyer, order.itemPrice, itemCount, order.itemCount); } } diff --git a/test/marketplace/lsp7/LSP7Orders.t.sol b/test/marketplace/lsp7/LSP7Orders.t.sol index 5c8a9e9..1504d8c 100644 --- a/test/marketplace/lsp7/LSP7Orders.t.sol +++ b/test/marketplace/lsp7/LSP7Orders.t.sol @@ -86,9 +86,9 @@ contract LSP7OrdersTest is Test { vm.expectRevert("Pausable: paused"); orders.place(address(asset), 1 ether, 1); vm.expectRevert("Pausable: paused"); - orders.cancel(address(asset)); + orders.cancel(1); vm.expectRevert("Pausable: paused"); - orders.fill(address(asset), address(100), address(101), 100); + orders.fill(1, address(100), 100); } function testFuzz_NotPlacedOf(address someAsset, address buyer) public { @@ -150,7 +150,7 @@ contract LSP7OrdersTest is Test { vm.prank(address(alice)); vm.expectEmit(); emit Canceled(1, address(asset), address(alice), 0.5 ether, 2); - orders.cancel(address(asset)); + orders.cancel(1); assertFalse(orders.isPlacedOrder(1)); assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); @@ -169,7 +169,7 @@ contract LSP7OrdersTest is Test { vm.prank(buyer); vm.expectRevert(abi.encodeWithSelector(LSP7Orders.NotPlacedOf.selector, address(asset), buyer)); - orders.cancel(address(asset)); + orders.cancel(1); assertTrue(orders.isPlacedOrderOf(address(asset), address(alice))); } @@ -204,7 +204,7 @@ contract LSP7OrdersTest is Test { vm.prank(marketplace); vm.expectEmit(); emit Filled(1, address(asset), address(bob), address(alice), itemPrice, fillCount, itemCount); - orders.fill(address(asset), address(bob), address(alice), fillCount); + orders.fill(1, address(bob), fillCount); assertEq(marketplace.balance, fillCount * itemPrice); @@ -246,7 +246,7 @@ contract LSP7OrdersTest is Test { vm.prank(marketplace); vm.expectEmit(); emit Filled(1, address(asset), address(bob), address(alice), itemPrice, itemCount, itemCount); - orders.fill(address(asset), address(bob), address(alice), itemCount); + orders.fill(1, address(bob), itemCount); assertEq(marketplace.balance, itemCount * itemPrice); assertFalse(orders.isPlacedOrderOf(address(asset), address(alice))); From 5ed3b5d7e45bb33b0b1d55ecba0279f679ba1724 Mon Sep 17 00:00:00 2001 From: Volodymyr Lykhonis Date: Tue, 13 Feb 2024 15:25:30 +0100 Subject: [PATCH 8/8] Do not allow fill zero items --- src/marketplace/lsp7/LSP7Orders.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/marketplace/lsp7/LSP7Orders.sol b/src/marketplace/lsp7/LSP7Orders.sol index 7c086f6..813ccab 100644 --- a/src/marketplace/lsp7/LSP7Orders.sol +++ b/src/marketplace/lsp7/LSP7Orders.sol @@ -99,6 +99,9 @@ contract LSP7Orders is ILSP7Orders, Module { nonReentrant onlyMarketplace { + if (itemCount == 0) { + revert InvalidItemCount(itemCount); + } LSP7Order memory order = getOrder(id); if (itemCount > order.itemCount) { revert InsufficientItemCount(order.itemCount, itemCount);