From 7a58f4b255d94c2f4645e2b8548bcd0e214b9b68 Mon Sep 17 00:00:00 2001 From: hexlive Date: Mon, 20 Nov 2023 12:33:19 -0500 Subject: [PATCH 01/16] Init Mintra update --- .../prebuilts/marketplace/IMarketplace.sol | 9 + .../MintraDirectListingsLogicStandalone.sol | 624 ++++++++++++++++++ 2 files changed, 633 insertions(+) create mode 100644 contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol diff --git a/contracts/prebuilts/marketplace/IMarketplace.sol b/contracts/prebuilts/marketplace/IMarketplace.sol index 07e3e5ad4..9f1162d9f 100644 --- a/contracts/prebuilts/marketplace/IMarketplace.sol +++ b/contracts/prebuilts/marketplace/IMarketplace.sol @@ -113,6 +113,15 @@ interface IDirectListings { uint256 totalPricePaid ); + event RoyaltyTransfered( + address assetContract, + uint256 tokenId, + uint256 listingId, + uint256 totalPrice, + uint256 royaltyAmount, + address royaltyRecipient + ); + /** * @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. * diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol new file mode 100644 index 000000000..6e0304e77 --- /dev/null +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -0,0 +1,624 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +import "./DirectListingsStorage.sol"; + +// ====== External imports ====== +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "../../../eip/interface/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC2981.sol"; + +// ====== Internal imports ====== +import "../../../extension/Multicall.sol"; +import "../../../extension/interface/IPlatformFee.sol"; +import "../../../extension/upgradeable/ERC2771ContextConsumer.sol"; +import "../../../extension/upgradeable/ReentrancyGuard.sol"; +import "../../../extension/upgradeable/PermissionsEnumerable.sol"; +import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPayments.sol"; +import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; + +/** + * @author thirdweb.com + */ +contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, ReentrancyGuard, ERC2771ContextConsumer { + /*/////////////////////////////////////////////////////////////// + Mintra + //////////////////////////////////////////////////////////////*/ + struct Royalty { + address receiver; + uint256 basisPoints; + } + + address public wizard; + address private mintTokenAddress; + address public platformFeeRecipient; + uint256 public platformFeeBps = 225; + uint256 public platformFeeBpsMint = 150; + mapping(address => Royalty) public royalties; + + /*/////////////////////////////////////////////////////////////// + Constants / Immutables + //////////////////////////////////////////////////////////////*/ + + /// @dev The max bps of the contract. So, 10_000 == 100 % + uint64 private constant MAX_BPS = 10_000; + + /// @dev The address of the native token wrapper contract. + address private immutable nativeTokenWrapper; + + /*/////////////////////////////////////////////////////////////// + Modifier + //////////////////////////////////////////////////////////////*/ + + modifier onlyWizard() { + require(msg.sender == wizard, "Not Wizard"); + _; + } + + /// @dev Checks whether caller is a listing creator. + modifier onlyListingCreator(uint256 _listingId) { + require( + _directListingsStorage().listings[_listingId].listingCreator == _msgSender(), + "Marketplace: not listing creator." + ); + _; + } + + /// @dev Checks whether a listing exists. + modifier onlyExistingListing(uint256 _listingId) { + require( + _directListingsStorage().listings[_listingId].status == IDirectListings.Status.CREATED, + "Marketplace: invalid listing." + ); + _; + } + + /*/////////////////////////////////////////////////////////////// + Constructor logic + //////////////////////////////////////////////////////////////*/ + + constructor( + address _nativeTokenWrapper, + address _mintTokenAddress, + address _platformFeeRecipient, + address _wizard + ) { + nativeTokenWrapper = _nativeTokenWrapper; + mintTokenAddress = _mintTokenAddress; + platformFeeRecipient = _platformFeeRecipient; + wizard = _wizard; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. + function createListing(ListingParameters calldata _params) + external + returns (uint256 listingId) + { + listingId = _getNextListingId(); + address listingCreator = _msgSender(); + TokenType tokenType = _getTokenType(_params.assetContract); + + uint128 startTime = _params.startTimestamp; + uint128 endTime = _params.endTimestamp; + require(startTime < endTime, "Marketplace: endTimestamp not greater than startTimestamp."); + if (startTime < block.timestamp) { + require(startTime + 60 minutes >= block.timestamp, "Marketplace: invalid startTimestamp."); + + startTime = uint128(block.timestamp); + endTime = endTime == type(uint128).max + ? endTime + : startTime + (_params.endTimestamp - _params.startTimestamp); + } + + _validateNewListing(_params, tokenType); + + Listing memory listing = Listing({ + listingId: listingId, + listingCreator: listingCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + pricePerToken: _params.pricePerToken, + startTimestamp: startTime, + endTimestamp: endTime, + reserved: _params.reserved, + tokenType: tokenType, + status: IDirectListings.Status.CREATED + }); + + _directListingsStorage().listings[listingId] = listing; + + emit NewListing(listingCreator, listingId, _params.assetContract, listing); + } + + /// @notice Update parameters of a listing of NFTs. + function updateListing(uint256 _listingId, ListingParameters memory _params) + external + onlyExistingListing(_listingId) + onlyListingCreator(_listingId) + { + address listingCreator = _msgSender(); + Listing memory listing = _directListingsStorage().listings[_listingId]; + TokenType tokenType = _getTokenType(_params.assetContract); + + require(listing.endTimestamp > block.timestamp, "Marketplace: listing expired."); + + require( + listing.assetContract == _params.assetContract && listing.tokenId == _params.tokenId, + "Marketplace: cannot update what token is listed." + ); + + uint128 startTime = _params.startTimestamp; + uint128 endTime = _params.endTimestamp; + require(startTime < endTime, "Marketplace: endTimestamp not greater than startTimestamp."); + require( + listing.startTimestamp > block.timestamp || + (startTime == listing.startTimestamp && endTime > block.timestamp), + "Marketplace: listing already active." + ); + if (startTime != listing.startTimestamp && startTime < block.timestamp) { + require(startTime + 60 minutes >= block.timestamp, "Marketplace: invalid startTimestamp."); + + startTime = uint128(block.timestamp); + + endTime = endTime == listing.endTimestamp || endTime == type(uint128).max + ? endTime + : startTime + (_params.endTimestamp - _params.startTimestamp); + } + + { + uint256 _approvedCurrencyPrice = _directListingsStorage().currencyPriceForListing[_listingId][ + _params.currency + ]; + require( + _approvedCurrencyPrice == 0 || _params.pricePerToken == _approvedCurrencyPrice, + "Marketplace: price different from approved price" + ); + } + + _validateNewListing(_params, tokenType); + + listing = Listing({ + listingId: _listingId, + listingCreator: listingCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + pricePerToken: _params.pricePerToken, + startTimestamp: startTime, + endTimestamp: endTime, + reserved: _params.reserved, + tokenType: tokenType, + status: IDirectListings.Status.CREATED + }); + + _directListingsStorage().listings[_listingId] = listing; + + emit UpdatedListing(listingCreator, _listingId, _params.assetContract, listing); + } + + /// @notice Cancel a listing. + function cancelListing(uint256 _listingId) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + _directListingsStorage().listings[_listingId].status = IDirectListings.Status.CANCELLED; + emit CancelledListing(_msgSender(), _listingId); + } + + /// @notice Approve a buyer to buy from a reserved listing. + function approveBuyerForListing( + uint256 _listingId, + address _buyer, + bool _toApprove + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + require(_directListingsStorage().listings[_listingId].reserved, "Marketplace: listing not reserved."); + + _directListingsStorage().isBuyerApprovedForListing[_listingId][_buyer] = _toApprove; + + emit BuyerApprovedForListing(_listingId, _buyer, _toApprove); + } + + /// @notice Approve a currency as a form of payment for the listing. + function approveCurrencyForListing( + uint256 _listingId, + address _currency, + uint256 _pricePerTokenInCurrency + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + Listing memory listing = _directListingsStorage().listings[_listingId]; + require( + _currency != listing.currency || _pricePerTokenInCurrency == listing.pricePerToken, + "Marketplace: approving listing currency with different price." + ); + require( + _directListingsStorage().currencyPriceForListing[_listingId][_currency] != _pricePerTokenInCurrency, + "Marketplace: price unchanged." + ); + + _directListingsStorage().currencyPriceForListing[_listingId][_currency] = _pricePerTokenInCurrency; + + emit CurrencyApprovedForListing(_listingId, _currency, _pricePerTokenInCurrency); + } + + /// @notice Buy NFTs from a listing. + function buyFromListing( + uint256 _listingId, + address _buyFor, + uint256 _quantity, + address _currency, + uint256 _expectedTotalPrice + ) external payable nonReentrant onlyExistingListing(_listingId) { + Listing memory listing = _directListingsStorage().listings[_listingId]; + address buyer = _msgSender(); + + require( + !listing.reserved || _directListingsStorage().isBuyerApprovedForListing[_listingId][buyer], + "buyer not approved" + ); + require(_quantity > 0 && _quantity <= listing.quantity, "Buying invalid quantity"); + require( + block.timestamp < listing.endTimestamp && block.timestamp >= listing.startTimestamp, + "not within sale window." + ); + + require( + _validateOwnershipAndApproval( + listing.listingCreator, + listing.assetContract, + listing.tokenId, + _quantity, + listing.tokenType + ), + "Marketplace: not owner or approved tokens." + ); + + uint256 targetTotalPrice; + + if (_directListingsStorage().currencyPriceForListing[_listingId][_currency] > 0) { + targetTotalPrice = _quantity * _directListingsStorage().currencyPriceForListing[_listingId][_currency]; + } else { + require(_currency == listing.currency, "Paying in invalid currency."); + targetTotalPrice = _quantity * listing.pricePerToken; + } + + require(targetTotalPrice == _expectedTotalPrice, "Unexpected total price"); + + // Check: buyer owns and has approved sufficient currency for sale. + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + require(msg.value == targetTotalPrice, "Marketplace: msg.value must exactly be the total price."); + } else { + require(msg.value == 0, "Marketplace: invalid native tokens sent."); + _validateERC20BalAndAllowance(buyer, _currency, targetTotalPrice); + } + + if (listing.quantity == _quantity) { + _directListingsStorage().listings[_listingId].status = IDirectListings.Status.COMPLETED; + } + _directListingsStorage().listings[_listingId].quantity -= _quantity; + + _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); + _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); + + emit NewSale( + listing.listingCreator, + listing.listingId, + listing.assetContract, + listing.tokenId, + buyer, + _quantity, + targetTotalPrice + ); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total number of listings created. + * @dev At any point, the return value is the ID of the next listing created. + */ + function totalListings() external view returns (uint256) { + return _directListingsStorage().totalListings; + } + + /// @notice Returns whether a buyer is approved for a listing. + function isBuyerApprovedForListing(uint256 _listingId, address _buyer) external view returns (bool) { + return _directListingsStorage().isBuyerApprovedForListing[_listingId][_buyer]; + } + + /// @notice Returns whether a currency is approved for a listing. + function isCurrencyApprovedForListing(uint256 _listingId, address _currency) external view returns (bool) { + return _directListingsStorage().currencyPriceForListing[_listingId][_currency] > 0; + } + + /// @notice Returns the price per token for a listing, in the given currency. + function currencyPriceForListing(uint256 _listingId, address _currency) external view returns (uint256) { + if (_directListingsStorage().currencyPriceForListing[_listingId][_currency] == 0) { + revert("Currency not approved for listing"); + } + + return _directListingsStorage().currencyPriceForListing[_listingId][_currency]; + } + + /// @notice Returns all non-cancelled listings. + function getAllListings(uint256 _startId, uint256 _endId) external view returns (Listing[] memory _allListings) { + require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); + + _allListings = new Listing[](_endId - _startId + 1); + + for (uint256 i = _startId; i <= _endId; i += 1) { + _allListings[i - _startId] = _directListingsStorage().listings[i]; + } + } + + /** + * @notice Returns all valid listings between the start and end Id (both inclusive) provided. + * A valid listing is where the listing creator still owns and has approved Marketplace + * to transfer the listed NFTs. + */ + function getAllValidListings(uint256 _startId, uint256 _endId) + external + view + returns (Listing[] memory _validListings) + { + require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); + + Listing[] memory _listings = new Listing[](_endId - _startId + 1); + uint256 _listingCount; + + for (uint256 i = _startId; i <= _endId; i += 1) { + _listings[i - _startId] = _directListingsStorage().listings[i]; + if (_validateExistingListing(_listings[i - _startId])) { + _listingCount += 1; + } + } + + _validListings = new Listing[](_listingCount); + uint256 index = 0; + uint256 count = _listings.length; + for (uint256 i = 0; i < count; i += 1) { + if (_validateExistingListing(_listings[i])) { + _validListings[index++] = _listings[i]; + } + } + } + + /// @notice Returns a listing at a particular listing ID. + function getListing(uint256 _listingId) external view returns (Listing memory listing) { + listing = _directListingsStorage().listings[_listingId]; + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the next listing Id. + function _getNextListingId() internal returns (uint256 id) { + id = _directListingsStorage().totalListings; + _directListingsStorage().totalListings += 1; + } + + /// @dev Returns the interface supported by a contract. + function _getTokenType(address _assetContract) internal view returns (TokenType tokenType) { + if (IERC165(_assetContract).supportsInterface(type(IERC1155).interfaceId)) { + tokenType = TokenType.ERC1155; + } else if (IERC165(_assetContract).supportsInterface(type(IERC721).interfaceId)) { + tokenType = TokenType.ERC721; + } else { + revert("Marketplace: listed token must be ERC1155 or ERC721."); + } + } + + /// @dev Checks whether the listing creator owns and has approved marketplace to transfer listed tokens. + function _validateNewListing(ListingParameters memory _params, TokenType _tokenType) internal view { + require(_params.quantity > 0, "Marketplace: listing zero quantity."); + require(_params.quantity == 1 || _tokenType == TokenType.ERC1155, "Marketplace: listing invalid quantity."); + + require( + _validateOwnershipAndApproval( + _msgSender(), + _params.assetContract, + _params.tokenId, + _params.quantity, + _tokenType + ), + "Marketplace: not owner or approved tokens." + ); + } + + /// @dev Checks whether the listing exists, is active, and if the lister has sufficient balance. + function _validateExistingListing(Listing memory _targetListing) internal view returns (bool isValid) { + isValid = + _targetListing.startTimestamp <= block.timestamp && + _targetListing.endTimestamp > block.timestamp && + _targetListing.status == IDirectListings.Status.CREATED && + _validateOwnershipAndApproval( + _targetListing.listingCreator, + _targetListing.assetContract, + _targetListing.tokenId, + _targetListing.quantity, + _targetListing.tokenType + ); + } + + /// @dev Validates that `_tokenOwner` owns and has approved Marketplace to transfer NFTs. + function _validateOwnershipAndApproval( + address _tokenOwner, + address _assetContract, + uint256 _tokenId, + uint256 _quantity, + TokenType _tokenType + ) internal view returns (bool isValid) { + address market = address(this); + + if (_tokenType == TokenType.ERC1155) { + isValid = + IERC1155(_assetContract).balanceOf(_tokenOwner, _tokenId) >= _quantity && + IERC1155(_assetContract).isApprovedForAll(_tokenOwner, market); + } else if (_tokenType == TokenType.ERC721) { + address owner; + address operator; + + // failsafe for reverts in case of non-existent tokens + try IERC721(_assetContract).ownerOf(_tokenId) returns (address _owner) { + owner = _owner; + + // Nesting the approval check inside this try block, to run only if owner check doesn't revert. + // If the previous check for owner fails, then the return value will always evaluate to false. + try IERC721(_assetContract).getApproved(_tokenId) returns (address _operator) { + operator = _operator; + } catch {} + } catch {} + + isValid = + owner == _tokenOwner && + (operator == market || IERC721(_assetContract).isApprovedForAll(_tokenOwner, market)); + } + } + + /// @dev Validates that `_tokenOwner` owns and has approved Markeplace to transfer the appropriate amount of currency + function _validateERC20BalAndAllowance( + address _tokenOwner, + address _currency, + uint256 _amount + ) internal view { + require( + IERC20(_currency).balanceOf(_tokenOwner) >= _amount && + IERC20(_currency).allowance(_tokenOwner, address(this)) >= _amount, + "!BAL20" + ); + } + + /// @dev Transfers tokens listed for sale in a direct or auction listing. + function _transferListingTokens( + address _from, + address _to, + uint256 _quantity, + Listing memory _listing + ) internal { + if (_listing.tokenType == TokenType.ERC1155) { + IERC1155(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, _quantity, ""); + } else if (_listing.tokenType == TokenType.ERC721) { + IERC721(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, ""); + } + } + + /// @dev Pays out stakeholders in a sale. + function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Listing memory _listing + ) internal { + address _nativeTokenWrapper = nativeTokenWrapper; + uint256 amountRemaining; + + // Payout platform fee + { + uint256 platformFeeCut; + + // Descrease platform fee for mint token + if (_currencyToUse == mintTokenAddress) { + platformFeeCut = (_totalPayoutAmount * platformFeeBpsMint) / MAX_BPS; + } else { + platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS; + } + + // Transfer platform fee + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + platformFeeRecipient, + platformFeeCut, + _nativeTokenWrapper + ); + + amountRemaining = _totalPayoutAmount - platformFeeCut; + } + + // Payout royalties + { + // Get royalty recipients and amounts + (address royaltyRecipient, uint256 royaltyAmount) = processRoyalty( + _listing.assetContract, + _listing.tokenId, + _totalPayoutAmount + ); + + if (royaltyAmount > 0) { + // Check payout amount remaining is enough to cover royalty payment + require(amountRemaining >= royaltyAmount, "fees exceed the price"); + + // Transfer royalty + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + royaltyRecipient, + royaltyAmount, + _nativeTokenWrapper + ); + + emit RoyaltyTransfered( + _listing.assetContract, + _listing.tokenId, + _listing.listingId, + _totalPayoutAmount, + royaltyAmount, + royaltyRecipient + ); + } + } + + // Distribute price to token owner + CurrencyTransferLib.transferCurrencyWithWrapper( + _currencyToUse, + _payer, + _payee, + amountRemaining, + _nativeTokenWrapper + ); + } + + function processRoyalty( + address _tokenAddress, + uint256 _tokenId, + uint256 _price + ) internal view returns (address royaltyReceiver, uint256 royaltyAmount) { + // Check if collection has royalty using ERC2981 + if (isERC2981(_tokenAddress)) { + (royaltyReceiver, royaltyAmount) = IERC2981(_tokenAddress).royaltyInfo(_tokenId, _price); + } else { + royaltyAmount = (_price * royalties[_tokenAddress].basisPoints) / 10000; + royaltyReceiver = royalties[_tokenAddress].receiver; + } + + return (royaltyReceiver, royaltyAmount); + } + + /** + * @notice This function checks if a given contract is ERC2981 compliant + * @dev This function is called internally and cannot be accessed outside the contract + * @param _contract The address of the contract to check + * @return A boolean indicating whether the contract is ERC2981 compliant or not + */ + function isERC2981(address _contract) internal view returns (bool) { + try IERC2981(_contract).royaltyInfo(0, 0) returns (address, uint256) { + return true; + } catch { + return false; + } + } + + /// @dev Returns the DirectListings storage. + function _directListingsStorage() internal pure returns (DirectListingsStorage.Data storage data) { + data = DirectListingsStorage.data(); + } +} From 3b83ff1551eecafe46becdf90153d90c2d86e5c7 Mon Sep 17 00:00:00 2001 From: hexlive Date: Mon, 20 Nov 2023 16:10:18 -0500 Subject: [PATCH 02/16] Add ability to update marketplace fee --- .../MintraDirectListingsLogicStandalone.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 6e0304e77..3f143833c 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -621,4 +621,16 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen function _directListingsStorage() internal pure returns (DirectListingsStorage.Data storage data) { data = DirectListingsStorage.data(); } + + + /** + * @notice Update the market fee percentage + * @dev Updates the market fee percentage to a new value + * @param _platformFeeBps New value for the market fee percentage + */ + function setMarketPercent(uint256 _platformFeeBps) public onlyWizard { + require(_platformFeeBps <= 369, "Fee not in range"); + + platformFeeBps = _platformFeeBps; + } } From 4edc98acce3ec7d7c5eeeb417000ca1b2a34eae5 Mon Sep 17 00:00:00 2001 From: hexlive Date: Tue, 21 Nov 2023 14:01:42 -0500 Subject: [PATCH 03/16] Remove erc2271 --- .../MintraDirectListingsLogicStandalone.sol | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 3f143833c..34c1a83b3 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -23,7 +23,7 @@ import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; /** * @author thirdweb.com */ -contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, ReentrancyGuard, ERC2771ContextConsumer { +contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, ReentrancyGuard { /*/////////////////////////////////////////////////////////////// Mintra //////////////////////////////////////////////////////////////*/ @@ -32,9 +32,9 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 basisPoints; } - address public wizard; - address private mintTokenAddress; - address public platformFeeRecipient; + address public immutable wizard; + address private immutable mintTokenAddress; + address public immutable platformFeeRecipient; uint256 public platformFeeBps = 225; uint256 public platformFeeBpsMint = 150; mapping(address => Royalty) public royalties; @@ -61,7 +61,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen /// @dev Checks whether caller is a listing creator. modifier onlyListingCreator(uint256 _listingId) { require( - _directListingsStorage().listings[_listingId].listingCreator == _msgSender(), + _directListingsStorage().listings[_listingId].listingCreator == msg.sender, "Marketplace: not listing creator." ); _; @@ -102,7 +102,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen returns (uint256 listingId) { listingId = _getNextListingId(); - address listingCreator = _msgSender(); + address listingCreator = msg.sender; TokenType tokenType = _getTokenType(_params.assetContract); uint128 startTime = _params.startTimestamp; @@ -137,6 +137,8 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen _directListingsStorage().listings[listingId] = listing; emit NewListing(listingCreator, listingId, _params.assetContract, listing); + + return listingId; } /// @notice Update parameters of a listing of NFTs. @@ -145,7 +147,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen onlyExistingListing(_listingId) onlyListingCreator(_listingId) { - address listingCreator = _msgSender(); + address listingCreator = msg.sender; Listing memory listing = _directListingsStorage().listings[_listingId]; TokenType tokenType = _getTokenType(_params.assetContract); @@ -209,7 +211,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen /// @notice Cancel a listing. function cancelListing(uint256 _listingId) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { _directListingsStorage().listings[_listingId].status = IDirectListings.Status.CANCELLED; - emit CancelledListing(_msgSender(), _listingId); + emit CancelledListing(msg.sender, _listingId); } /// @notice Approve a buyer to buy from a reserved listing. @@ -255,7 +257,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 _expectedTotalPrice ) external payable nonReentrant onlyExistingListing(_listingId) { Listing memory listing = _directListingsStorage().listings[_listingId]; - address buyer = _msgSender(); + address buyer = msg.sender; require( !listing.reserved || _directListingsStorage().isBuyerApprovedForListing[_listingId][buyer], @@ -423,7 +425,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen require( _validateOwnershipAndApproval( - _msgSender(), + msg.sender, _params.assetContract, _params.tokenId, _params.quantity, @@ -628,7 +630,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen * @dev Updates the market fee percentage to a new value * @param _platformFeeBps New value for the market fee percentage */ - function setMarketPercent(uint256 _platformFeeBps) public onlyWizard { + function setPlatformFeeBps(uint256 _platformFeeBps) public onlyWizard { require(_platformFeeBps <= 369, "Fee not in range"); platformFeeBps = _platformFeeBps; From 311d13d1863f5217da8671dffae2340ff7e24dca Mon Sep 17 00:00:00 2001 From: hexlive Date: Tue, 21 Nov 2023 19:39:25 -0500 Subject: [PATCH 04/16] Add ability to add or update a mintra native royalty --- .../prebuilts/marketplace/IMarketplace.sol | 2 ++ .../MintraDirectListingsLogicStandalone.sol | 33 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/contracts/prebuilts/marketplace/IMarketplace.sol b/contracts/prebuilts/marketplace/IMarketplace.sol index 9f1162d9f..905106a0b 100644 --- a/contracts/prebuilts/marketplace/IMarketplace.sol +++ b/contracts/prebuilts/marketplace/IMarketplace.sol @@ -122,6 +122,8 @@ interface IDirectListings { address royaltyRecipient ); + event RoyaltyUpdated(address assetContract, uint256 royaltyAmount, address royaltyRecipient); + /** * @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. * diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 34c1a83b3..8834035b2 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "../../../eip/interface/IERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // ====== Internal imports ====== import "../../../extension/Multicall.sol"; @@ -97,10 +98,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen //////////////////////////////////////////////////////////////*/ /// @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. - function createListing(ListingParameters calldata _params) - external - returns (uint256 listingId) - { + function createListing(ListingParameters calldata _params) external returns (uint256 listingId) { listingId = _getNextListingId(); address listingCreator = msg.sender; TokenType tokenType = _getTokenType(_params.assetContract); @@ -397,6 +395,32 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen listing = _directListingsStorage().listings[_listingId]; } + /** + * @notice Set or update the royalty for a collection + * @dev Sets or updates the royalty for a collection to a new value + * @param _collectionAddress Address of the collection to set the royalty for + * @param _royaltyInBasisPoints New royalty value, in basis points (1 basis point = 0.01%) + */ + function createOrUpdateRoyalty( + address _collectionAddress, + uint256 _royaltyInBasisPoints, + address receiver + ) public nonReentrant { + require(_collectionAddress != address(0), "_collectionAddress is not set"); + require(_royaltyInBasisPoints >= 0 && _royaltyInBasisPoints <= 10000, "Royalty not in range"); + require(receiver != address(0), "receiver is not set"); + + // Check that the caller is the owner/creator of the collection contract + require(Ownable(_collectionAddress).owner() == msg.sender, "Unauthorized"); + + // Create a new Royalty object with the given value and store it in the royalties mapping + Royalty memory royalty = Royalty(receiver, _royaltyInBasisPoints); + royalties[_collectionAddress] = royalty; + + // Emit a RoyaltyUpdated + emit RoyaltyUpdated(_collectionAddress, _royaltyInBasisPoints, receiver); + } + /*/////////////////////////////////////////////////////////////// Internal functions //////////////////////////////////////////////////////////////*/ @@ -624,7 +648,6 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen data = DirectListingsStorage.data(); } - /** * @notice Update the market fee percentage * @dev Updates the market fee percentage to a new value From 0e7a5bf24352b3a74a08e488a7108142b797e221 Mon Sep 17 00:00:00 2001 From: hexlive Date: Wed, 22 Nov 2023 09:57:13 -0500 Subject: [PATCH 05/16] Fix issue with payout. Start with some foundry tests --- .vscode/settings.json | 4 + .../MintraDirectListingsLogicStandalone.sol | 2 + .../MintraDirectListingStandalone.t.sol | 1677 +++++++++++++++++ 3 files changed, 1683 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/test/marketplace/MintraDirectListingStandalone.t.sol diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..56a7d8c30 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "solidity.packageDefaultDependenciesContractsDirectory": "src", + "solidity.packageDefaultDependenciesDirectory": "lib" +} diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 8834035b2..1f9295948 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -592,6 +592,8 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen _nativeTokenWrapper ); + amountRemaining = amountRemaining - royaltyAmount; + emit RoyaltyTransfered( _listing.assetContract, _listing.tokenId, diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol new file mode 100644 index 000000000..03bf7e187 --- /dev/null +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -0,0 +1,1677 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../utils/BaseTest.sol"; + +// Test contracts and interfaces +import {RoyaltyPaymentsLogic} from "contracts/extension/plugin/RoyaltyPayments.sol"; +import {MarketplaceV3, IPlatformFee} from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import {TWProxy} from "contracts/infra/TWProxy.sol"; +import {ERC721Base} from "contracts/base/ERC721Base.sol"; +import {MockRoyaltyEngineV1} from "../mocks/MockRoyaltyEngineV1.sol"; + +import {IDirectListings} from "contracts/prebuilts/marketplace/IMarketplace.sol"; +import {MintraDirectListingsLogicStandalone} from + "contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + address public wizard; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + wizard = getActor(4); + + // Deploy implementation. + marketplace = address( + new MintraDirectListingsLogicStandalone( + address(weth), + address(erc20Aux), + address(platformFeeRecipient), + address(wizard) + ) + ); + + vm.prank(marketplaceDeployer); + // marketplace = address( + // new TWProxy( + // impl, + // abi.encodeCall( + // MarketplaceV3.initialize, + // (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + // ) + // ) + // ); + + //vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function test_state_initial() public { + uint256 totalListings = MintraDirectListingsLogicStandalone(marketplace).totalListings(); + assertEq(totalListings, 0); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_getValidListings_burnListedTokens() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + + // Total listings incremented + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + + // burn listed token + vm.prank(seller); + erc721.burn(0); + + vm.warp(150); + // Fetch listing and verify state. + uint256 totalListings = MintraDirectListingsLogicStandalone(marketplace).totalListings(); + assertEq(MintraDirectListingsLogicStandalone(marketplace).getAllValidListings(0, totalListings - 1).length, 0); + } + + /** + * @dev Tests contract state for Lister role. + */ + function test_state_getRoleMember_listerRole() public { + bytes32 role = keccak256("LISTER_ROLE"); + + uint256 roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 1); + + address roleMember = PermissionsEnumerable(marketplace).getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).grantRole(role, address(2)); + Permissions(marketplace).grantRole(role, address(3)); + Permissions(marketplace).grantRole(role, address(4)); + + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(2)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 3); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).grantRole(role, address(5)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).grantRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).grantRole(role, address(6)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 6); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(3)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 5); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(4)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 4); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + Permissions(marketplace).revokeRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); + assertEq(roleMemberCount, 3); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); + } + console.log(""); + + vm.stopPrank(); + } + + function test_state_approvedCurrencies() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParams) = _setup_updateListing(); + address currencyToApprove = address(erc20); // same currency as main listing + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves currency for listing. + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( + listingId, currencyToApprove, pricePerTokenForCurrency + ); + + // change currency + currencyToApprove = NATIVE_TOKEN; + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( + listingId, currencyToApprove, pricePerTokenForCurrency + ); + + assertEq( + MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), true + ); + assertEq( + MintraDirectListingsLogicStandalone(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), + pricePerTokenForCurrency + ); + + // should revert when updating listing with an approved currency but different price + listingParams.currency = NATIVE_TOKEN; + vm.prank(seller); + vm.expectRevert("Marketplace: price different from approved price"); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParams); + + // change listingParams.pricePerToken to approved price + listingParams.pricePerToken = pricePerTokenForCurrency; + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParams); + } + + /*/////////////////////////////////////////////////////////////// + Royalty Tests (incl Royalty Engine / Registry) + //////////////////////////////////////////////////////////////*/ + + function _setupRoyaltyEngine() + private + returns ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory mockRecipients, + uint256[] memory mockAmounts + ) + { + mockRecipients = new address payable[](2); + mockAmounts = new uint256[](2); + + mockRecipients[0] = payable(address(0x12345)); + mockRecipients[1] = payable(address(0x56789)); + + mockAmounts[0] = 10; + mockAmounts[1] = 15; + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = erc721TokenAddress; + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 100 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function _buyFromListingForRoyaltyTests(uint256 listingId) private returns (uint256 totalPrice) { + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + uint256 bla = erc20.allowance(buyer, marketplace); + + console.log(totalPrice); + console.log("bla1"); + console.log(bla); + console.log(listing.currency); + console.log(address(erc20)); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + console.log("done"); + } + + function test_royaltyEngine_tokenWithCustomRoyalties() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // 1. ========= Create listing ========= + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after royalty payments ======== + + { + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq(address(erc20), seller, totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1]); + } + } + + function test_royaltyEngine_tokenWithERC2981() public { + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + // Mint the ERC721 tokens to seller. These tokens will be listed. + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + //vm.prank(marketplaceDeployer); + + // 1. ========= Create listing ========= + + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount); + } + } + + function test_noRoyaltyEngine_defaultERC2981Token() public { + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + // 1. ========= Create listing ========= + + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); + console.log("here"); + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + console.log("here11"); + // 3. ======== Check balances after royalty payments ======== + + { + uint256 platforfee = (platformFeeBps * totalPrice) / 10_000; + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + + assertBalERC20Eq(address(erc20), platformFeeRecipient, platforfee); + console.log("platforfee: %s", platforfee); + + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + console.log("here2"); + // Seller gets total price minus royalty amount minus platform fee + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - platforfee); + console.log("here3"); + } + } + + function test_royaltyEngine_correctlyDistributeAllFees() public { + ( + MockRoyaltyEngineV1 royaltyEngine, + address payable[] memory customRoyaltyRecipients, + uint256[] memory customRoyaltyAmounts + ) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 5; + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Create listing ========= + + _setupERC721BalanceForSeller(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after fee payments (platform fee + royalty) ======== + + { + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); + assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); + + // Platform fee recipient + uint256 platformFeeAmount = (platformFeeBps * totalPrice) / 10_000; + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFeeAmount); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq( + address(erc20), + seller, + totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - platformFeeAmount + ); + } + } + + function test_revert_feesExceedTotalPrice() public { + (MockRoyaltyEngineV1 royaltyEngine,,) = _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + // Set platform fee on marketplace + address platformFeeRecipient = marketplaceDeployer; + uint128 platformFeeBps = 10_000; // equal to max bps 10_000 or 100% + vm.prank(marketplaceDeployer); + IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + + // 1. ========= Create listing ========= + + _setupERC721BalanceForSeller(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + + // 2. ========= Buy from listing ========= + + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + + vm.expectRevert("fees exceed the price"); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + } + + /*/////////////////////////////////////////////////////////////// + Create listing + //////////////////////////////////////////////////////////////*/ + + function test_state_createListing() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + uint256 listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings incremented + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, startTimestamp); + assertEq(listing.endTimestamp, endTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_revert_createListing_notOwnerOfListedToken() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Don't mint to 'token to be listed' to the seller. + address someWallet = getActor(1000); + _setupERC721BalanceForSeller(someWallet, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), someWallet, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(someWallet); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_notApprovedMarketplaceToTransferToken() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingZeroQuantity() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 0; // Listing ZERO quantity + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingInvalidQuantity() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 2; // Listing more than `1` quantity + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing invalid quantity."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_invalidStartTimestamp() public { + uint256 blockTimestamp = 100 minutes; + // Set block.timestamp + vm.warp(blockTimestamp); + + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = uint128(blockTimestamp - 61 minutes); // start time is less than block timestamp. + uint128 endTimestamp = uint128(startTimestamp + 1); + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid startTimestamp."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_invalidEndTimestamp() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = uint128(startTimestamp - 1); // End timestamp is less than start timestamp. + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingNonERC721OrERC1155Token() public { + // Sample listing parameters. + address assetContract = address(erc20); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + // Grant ERC20 token asset role. + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); + + vm.prank(seller); + vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_revert_createListing_noAssetRoleWhenRestrictionsActive() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + // Revoke ASSET_ROLE from token to list. + vm.startPrank(marketplaceDeployer); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(0)), false); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.stopPrank(); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + /*/////////////////////////////////////////////////////////////// + Update listing + //////////////////////////////////////////////////////////////*/ + + function _setup_updateListing() + private + returns (uint256 listingId, IDirectListings.ListingParameters memory listingParams) + { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(seller); + listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_state_updateListing() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.pricePerToken = 2 ether; + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings not incremented on update. + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, listingParamsToUpdate.assetContract); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, listingParamsToUpdate.quantity); + assertEq(listing.currency, listingParamsToUpdate.currency); + assertEq(listing.pricePerToken, listingParamsToUpdate.pricePerToken); + assertEq(listing.startTimestamp, listingParamsToUpdate.startTimestamp); + assertEq(listing.endTimestamp, listingParamsToUpdate.endTimestamp); + assertEq(listing.reserved, listingParamsToUpdate.reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_revert_updateListing_notListingCreator() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + address notSeller = getActor(1000); // Someone other than the seller calls update. + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_notOwnerOfListedToken() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens but NOT to seller. A new tokenId will be listed. + address notSeller = getActor(1000); + _setupERC721BalanceForSeller(notSeller, 1); + + // Approve Marketplace to transfer token. + vm.prank(notSeller); + erc721.setApprovalForAll(marketplace, true); + + // Transfer away owned token. + vm.prank(seller); + erc721.transferFrom(seller, address(0x1234), 0); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_notApprovedMarketplaceToTransferToken() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingZeroQuantity() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.quantity = 0; // Listing zero quantity + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingInvalidQuantity() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.quantity = 2; // Listing more than `1` of the ERC721 token + + vm.prank(seller); + vm.expectRevert("Marketplace: listing invalid quantity."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingNonERC721OrERC1155Token() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.assetContract = address(erc20); // Listing non ERC721 / ERC1155 token. + + // Grant ERC20 token asset role. + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); + + vm.prank(seller); + vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_invalidStartTimestamp() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + uint128 currentStartTimestamp = listingParamsToUpdate.startTimestamp; + listingParamsToUpdate.startTimestamp = currentStartTimestamp - 1; // Retroactively decreasing startTimestamp. + + vm.warp(currentStartTimestamp + 50); + vm.prank(seller); + vm.expectRevert("Marketplace: listing already active."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_invalidEndTimestamp() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + uint128 currentStartTimestamp = listingParamsToUpdate.startTimestamp; + listingParamsToUpdate.endTimestamp = currentStartTimestamp - 1; // End timestamp less than startTimestamp + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + /*/////////////////////////////////////////////////////////////// + Cancel listing + //////////////////////////////////////////////////////////////*/ + + function _setup_cancelListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId,) = _setup_updateListing(); + listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + } + + function test_state_cancelListing() public { + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(); + + // Verify existing listing at `listingId` + assertEq(existingListingAtId.assetContract, address(erc721)); + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).cancelListing(listingId); + + // status should be `CANCELLED` + IDirectListings.Listing memory cancelledListing = + MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + assertTrue(cancelledListing.status == IDirectListings.Status.CANCELLED); + } + + function test_revert_cancelListing_notListingCreator() public { + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(); + + // Verify existing listing at `listingId` + assertEq(existingListingAtId.assetContract, address(erc721)); + + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListingsLogicStandalone(marketplace).cancelListing(listingId); + } + + function test_revert_cancelListing_nonExistentListing() public { + _setup_cancelListing(); + + // Verify no listing exists at `nexListingId` + uint256 nextListingId = MintraDirectListingsLogicStandalone(marketplace).totalListings(); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + MintraDirectListingsLogicStandalone(marketplace).cancelListing(nextListingId); + } + + /*/////////////////////////////////////////////////////////////// + Approve buyer for listing + //////////////////////////////////////////////////////////////*/ + + function _setup_approveBuyerForListing() private returns (uint256 listingId) { + (listingId,) = _setup_updateListing(); + } + + function test_state_approveBuyerForListing() public { + uint256 listingId = _setup_approveBuyerForListing(); + bool toApprove = true; + + assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, true); + + // Seller approves buyer for reserved listing. + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + + assertEq(MintraDirectListingsLogicStandalone(marketplace).isBuyerApprovedForListing(listingId, buyer), true); + } + + function test_revert_approveBuyerForListing_notListingCreator() public { + uint256 listingId = _setup_approveBuyerForListing(); + bool toApprove = true; + + assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, true); + + // Someone other than the seller approves buyer for reserved listing. + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + } + + function test_revert_approveBuyerForListing_listingNotReserved() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + bool toApprove = true; + + assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, true); + + listingParamsToUpdate.reserved = false; + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + + assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, false); + + // Seller approves buyer for reserved listing. + vm.prank(seller); + vm.expectRevert("Marketplace: listing not reserved."); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + } + + /*/////////////////////////////////////////////////////////////// + Approve currency for listing + //////////////////////////////////////////////////////////////*/ + + function _setup_approveCurrencyForListing() private returns (uint256 listingId) { + (listingId,) = _setup_updateListing(); + } + + function test_state_approveCurrencyForListing() public { + uint256 listingId = _setup_approveCurrencyForListing(); + address currencyToApprove = NATIVE_TOKEN; + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves buyer for reserved listing. + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( + listingId, currencyToApprove, pricePerTokenForCurrency + ); + + assertEq( + MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), true + ); + assertEq( + MintraDirectListingsLogicStandalone(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), + pricePerTokenForCurrency + ); + } + + function test_revert_approveCurrencyForListing_notListingCreator() public { + uint256 listingId = _setup_approveCurrencyForListing(); + address currencyToApprove = NATIVE_TOKEN; + uint256 pricePerTokenForCurrency = 2 ether; + + // Someone other than seller approves buyer for reserved listing. + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( + listingId, currencyToApprove, pricePerTokenForCurrency + ); + } + + function test_revert_approveCurrencyForListing_reApprovingMainCurrency() public { + uint256 listingId = _setup_approveCurrencyForListing(); + address currencyToApprove = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).currency; + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves buyer for reserved listing. + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( + listingId, currencyToApprove, pricePerTokenForCurrency + ); + } + + /*/////////////////////////////////////////////////////////////// + Buy from listing + //////////////////////////////////////////////////////////////*/ + + function _setup_buyFromListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId,) = _setup_updateListing(); + listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + } + + function test_state_buyFromListing() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + assertBalERC20Eq(address(erc20), seller, totalPrice); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = + MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_state_buyFromListing_nativeToken() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + uint256 buyerBalBefore = buyer.balance; + uint256 sellerBalBefore = seller.balance; + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: totalPrice}( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertEq(buyer.balance, buyerBalBefore - totalPrice); + assertEq(seller.balance, sellerBalBefore + totalPrice); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = + MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_revert_buyFromListing_nativeToken_incorrectValueSent() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Marketplace: msg.value must exactly be the total price."); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: totalPrice - 1}( // sending insufficient value + listingId, buyFor, quantityToBuy, currency, totalPrice); + } + + function test_revert_buyFromListing_unexpectedTotalPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Unexpected total price"); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: totalPrice}( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + 1 // Pass unexpected total price + ); + } + + function test_revert_buyFromListing_invalidCurrency() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + + assertEq(listing.currency, address(erc20)); + assertEq( + MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + false + ); + + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Paying in invalid currency."); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, NATIVE_TOKEN, totalPrice + ); + } + + function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price + assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + } + + function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.approve(marketplace, 0); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + } + + function test_revert_buyFromListing_buyingZeroQuantity() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = 0; // Buying zero quantity + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + } + + function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, buyFor, quantityToBuy, currency, totalPrice + ); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + function _createListing(address _seller) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(_seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), _seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(_seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + ); + + vm.prank(_seller); + listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + } + + function test_audit_native_tokens_locked() public { + (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingListing.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingListing.assetContract, address(erc721)); + + vm.warp(existingListing.startTimestamp); + + // No ether is locked in contract + assertEq(marketplace.balance, 0); + + // buy from listing + erc20.mint(buyer, 10 ether); + vm.deal(buyer, 1 ether); + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + vm.startPrank(buyer); + erc20.approve(marketplace, 10 ether); + + vm.expectRevert("Marketplace: invalid native tokens sent."); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: 1 ether}( + listingId, buyer, 1, address(erc20), 1 ether + ); + vm.stopPrank(); + + // 1 ether is temporary locked in contract + assertEq(marketplace.balance, 0 ether); + } +} From 2bd180a326701ff50d729dee83ccdc2e8014d7e2 Mon Sep 17 00:00:00 2001 From: hexlive Date: Thu, 23 Nov 2023 21:02:54 -0500 Subject: [PATCH 06/16] Remove some not needed tests --- .../MintraDirectListingStandalone.t.sol | 249 ------------------ 1 file changed, 249 deletions(-) diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index 03bf7e187..e729e15d0 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -115,97 +115,6 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assertEq(MintraDirectListingsLogicStandalone(marketplace).getAllValidListings(0, totalListings - 1).length, 0); } - /** - * @dev Tests contract state for Lister role. - */ - function test_state_getRoleMember_listerRole() public { - bytes32 role = keccak256("LISTER_ROLE"); - - uint256 roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 1); - - address roleMember = PermissionsEnumerable(marketplace).getRoleMember(role, 1); - assertEq(roleMember, address(0)); - - vm.startPrank(marketplaceDeployer); - Permissions(marketplace).grantRole(role, address(2)); - Permissions(marketplace).grantRole(role, address(3)); - Permissions(marketplace).grantRole(role, address(4)); - - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 4); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).revokeRole(role, address(2)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 3); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).grantRole(role, address(5)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 4); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).grantRole(role, address(0)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 5); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).grantRole(role, address(6)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 6); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).revokeRole(role, address(3)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 5); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).revokeRole(role, address(4)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 4); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - Permissions(marketplace).revokeRole(role, address(0)); - roleMemberCount = PermissionsEnumerable(marketplace).getRoleMemberCount(role); - assertEq(roleMemberCount, 3); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(PermissionsEnumerable(marketplace).getRoleMember(role, i)); - } - console.log(""); - - vm.stopPrank(); - } - function test_state_approvedCurrencies() public { (uint256 listingId, IDirectListings.ListingParameters memory listingParams) = _setup_updateListing(); address currencyToApprove = address(erc20); // same currency as main listing @@ -326,72 +235,6 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { console.log("done"); } - function test_royaltyEngine_tokenWithCustomRoyalties() public { - ( - MockRoyaltyEngineV1 royaltyEngine, - address payable[] memory customRoyaltyRecipients, - uint256[] memory customRoyaltyAmounts - ) = _setupRoyaltyEngine(); - - // Add RoyaltyEngine to marketplace - vm.prank(marketplaceDeployer); - RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); - - assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); - - // 1. ========= Create listing ========= - - // Mint the ERC721 tokens to seller. These tokens will be listed. - _setupERC721BalanceForSeller(seller, 1); - uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); - - // 2. ========= Buy from listing ========= - - uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); - - // 3. ======== Check balances after royalty payments ======== - - { - // Royalty recipients receive correct amounts - assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); - assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); - - // Seller gets total price minus royalty amounts - assertBalERC20Eq(address(erc20), seller, totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1]); - } - } - - function test_royaltyEngine_tokenWithERC2981() public { - // create token with ERC2981 - address royaltyRecipient = address(0x12345); - uint128 royaltyBps = 10; - ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); - // Mint the ERC721 tokens to seller. These tokens will be listed. - vm.prank(address(0x12345)); - nft2981.mintTo(seller, ""); - - //vm.prank(marketplaceDeployer); - - // 1. ========= Create listing ========= - - uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); - - // 2. ========= Buy from listing ========= - - uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); - - // 3. ======== Check balances after royalty payments ======== - - { - uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; - // Royalty recipient receives correct amounts - assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); - - // Seller gets total price minus royalty amount - assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount); - } - } - function test_noRoyaltyEngine_defaultERC2981Token() public { // create token with ERC2981 address royaltyRecipient = address(0x12345); @@ -428,54 +271,6 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } - function test_royaltyEngine_correctlyDistributeAllFees() public { - ( - MockRoyaltyEngineV1 royaltyEngine, - address payable[] memory customRoyaltyRecipients, - uint256[] memory customRoyaltyAmounts - ) = _setupRoyaltyEngine(); - - // Add RoyaltyEngine to marketplace - vm.prank(marketplaceDeployer); - RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); - - assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); - - // Set platform fee on marketplace - address platformFeeRecipient = marketplaceDeployer; - uint128 platformFeeBps = 5; - vm.prank(marketplaceDeployer); - IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); - - // 1. ========= Create listing ========= - - _setupERC721BalanceForSeller(seller, 1); - uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); - - // 2. ========= Buy from listing ========= - - uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); - - // 3. ======== Check balances after fee payments (platform fee + royalty) ======== - - { - // Royalty recipients receive correct amounts - assertBalERC20Eq(address(erc20), customRoyaltyRecipients[0], customRoyaltyAmounts[0]); - assertBalERC20Eq(address(erc20), customRoyaltyRecipients[1], customRoyaltyAmounts[1]); - - // Platform fee recipient - uint256 platformFeeAmount = (platformFeeBps * totalPrice) / 10_000; - assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFeeAmount); - - // Seller gets total price minus royalty amounts - assertBalERC20Eq( - address(erc20), - seller, - totalPrice - customRoyaltyAmounts[0] - customRoyaltyAmounts[1] - platformFeeAmount - ); - } - } - function test_revert_feesExceedTotalPrice() public { (MockRoyaltyEngineV1 royaltyEngine,,) = _setupRoyaltyEngine(); @@ -795,55 +590,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved ); - // Grant ERC20 token asset role. - vm.prank(marketplaceDeployer); - Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); - vm.prank(seller); vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); } - function test_revert_createListing_noAssetRoleWhenRestrictionsActive() public { - // Sample listing parameters. - address assetContract = address(erc721); - uint256 tokenId = 0; - uint256 quantity = 1; - address currency = address(erc20); - uint256 pricePerToken = 1 ether; - uint128 startTimestamp = 100; - uint128 endTimestamp = 200; - bool reserved = true; - - // Mint the ERC721 tokens to seller. These tokens will be listed. - _setupERC721BalanceForSeller(seller, 1); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = tokenId; - assertIsOwnerERC721(address(erc721), seller, tokenIds); - - // Approve Marketplace to transfer token. - vm.prank(seller); - erc721.setApprovalForAll(marketplace, true); - - // List tokens. - IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved - ); - - // Revoke ASSET_ROLE from token to list. - vm.startPrank(marketplaceDeployer); - assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(0)), false); - Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(erc721)); - assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); - - vm.stopPrank(); - - vm.prank(seller); - vm.expectRevert("!ASSET_ROLE"); - MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); - } - /*/////////////////////////////////////////////////////////////// Update listing //////////////////////////////////////////////////////////////*/ From cae9a3422edac6bbeae0a7c5dfb4a0011cbf5952 Mon Sep 17 00:00:00 2001 From: hexlive Date: Tue, 28 Nov 2023 15:42:36 -0500 Subject: [PATCH 07/16] All tests passing --- .../prebuilts/marketplace/IMarketplace.sol | 11 - .../MintraDirectListingsLogicStandalone.sol | 36 +- .../MintraDirectListingStandalone.t.sol | 362 ++++++++++++------ src/test/mocks/MockERC721Ownable.sol | 25 ++ 4 files changed, 297 insertions(+), 137 deletions(-) create mode 100644 src/test/mocks/MockERC721Ownable.sol diff --git a/contracts/prebuilts/marketplace/IMarketplace.sol b/contracts/prebuilts/marketplace/IMarketplace.sol index 905106a0b..07e3e5ad4 100644 --- a/contracts/prebuilts/marketplace/IMarketplace.sol +++ b/contracts/prebuilts/marketplace/IMarketplace.sol @@ -113,17 +113,6 @@ interface IDirectListings { uint256 totalPricePaid ); - event RoyaltyTransfered( - address assetContract, - uint256 tokenId, - uint256 listingId, - uint256 totalPrice, - uint256 royaltyAmount, - address royaltyRecipient - ); - - event RoyaltyUpdated(address assetContract, uint256 royaltyAmount, address royaltyRecipient); - /** * @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. * diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 1f9295948..64543d1ce 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -22,7 +22,7 @@ import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPaym import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; /** - * @author thirdweb.com + * @author thirdweb.com / mintra.ai */ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, ReentrancyGuard { /*/////////////////////////////////////////////////////////////// @@ -33,6 +33,30 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 basisPoints; } + event NewSale( + address indexed listingCreator, + uint256 indexed listingId, + address indexed assetContract, + uint256 tokenId, + address buyer, + uint256 quantityBought, + uint256 totalPricePaid, + address currency + ); + + event RoyaltyTransfered( + address assetContract, + uint256 tokenId, + uint256 listingId, + uint256 totalPrice, + uint256 royaltyAmount, + uint256 platformFee, + address royaltyRecipient, + address currency + ); + + event RoyaltyUpdated(address assetContract, uint256 royaltyAmount, address royaltyRecipient); + address public immutable wizard; address private immutable mintTokenAddress; address public immutable platformFeeRecipient; @@ -312,7 +336,8 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen listing.tokenId, buyer, _quantity, - targetTotalPrice + targetTotalPrice, + _currency ); } @@ -546,11 +571,10 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen ) internal { address _nativeTokenWrapper = nativeTokenWrapper; uint256 amountRemaining; + uint256 platformFeeCut; // Payout platform fee { - uint256 platformFeeCut; - // Descrease platform fee for mint token if (_currencyToUse == mintTokenAddress) { platformFeeCut = (_totalPayoutAmount * platformFeeBpsMint) / MAX_BPS; @@ -600,7 +624,9 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen _listing.listingId, _totalPayoutAmount, royaltyAmount, - royaltyRecipient + platformFeeCut, + royaltyRecipient, + _currencyToUse ); } } diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index e729e15d0..375efde7d 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -5,16 +5,16 @@ pragma solidity ^0.8.0; import "../utils/BaseTest.sol"; // Test contracts and interfaces -import {RoyaltyPaymentsLogic} from "contracts/extension/plugin/RoyaltyPayments.sol"; -import {MarketplaceV3, IPlatformFee} from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; -import {TWProxy} from "contracts/infra/TWProxy.sol"; -import {ERC721Base} from "contracts/base/ERC721Base.sol"; -import {MockRoyaltyEngineV1} from "../mocks/MockRoyaltyEngineV1.sol"; - -import {IDirectListings} from "contracts/prebuilts/marketplace/IMarketplace.sol"; -import {MintraDirectListingsLogicStandalone} from - "contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol"; +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../mocks/MockRoyaltyEngineV1.sol"; + +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; +import { MintraDirectListingsLogicStandalone } from "contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol"; import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +import { MockERC721Ownable } from "../mocks/MockERC721Ownable.sol"; contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Target contract @@ -25,6 +25,10 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { address public seller; address public buyer; address public wizard; + address public collectionOwner; + + MintraDirectListingsLogicStandalone public mintraDirectListingsLogicStandalone; + MockERC721Ownable public erc721Ownable; function setUp() public override { super.setUp(); @@ -33,29 +37,22 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { seller = getActor(2); buyer = getActor(3); wizard = getActor(4); + collectionOwner = getActor(5); // Deploy implementation. - marketplace = address( - new MintraDirectListingsLogicStandalone( - address(weth), - address(erc20Aux), - address(platformFeeRecipient), - address(wizard) - ) + mintraDirectListingsLogicStandalone = new MintraDirectListingsLogicStandalone( + address(weth), + address(erc20Aux), + address(platformFeeRecipient), + address(wizard) ); + marketplace = address(mintraDirectListingsLogicStandalone); + + vm.prank(collectionOwner); + erc721Ownable = new MockERC721Ownable(); + + //vm.prank(marketplaceDeployer); - vm.prank(marketplaceDeployer); - // marketplace = address( - // new TWProxy( - // impl, - // abi.encodeCall( - // MarketplaceV3.initialize, - // (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) - // ) - // ) - // ); - - //vm.label(impl, "MarketplaceV3_Impl"); vm.label(marketplace, "Marketplace"); vm.label(seller, "Seller"); vm.label(buyer, "Buyer"); @@ -96,7 +93,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -124,7 +128,9 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(seller); vm.expectRevert("Marketplace: approving listing currency with different price."); MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( - listingId, currencyToApprove, pricePerTokenForCurrency + listingId, + currencyToApprove, + pricePerTokenForCurrency ); // change currency @@ -132,11 +138,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(seller); MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( - listingId, currencyToApprove, pricePerTokenForCurrency + listingId, + currencyToApprove, + pricePerTokenForCurrency ); assertEq( - MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), true + MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + true ); assertEq( MintraDirectListingsLogicStandalone(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), @@ -159,26 +168,6 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Royalty Tests (incl Royalty Engine / Registry) //////////////////////////////////////////////////////////////*/ - function _setupRoyaltyEngine() - private - returns ( - MockRoyaltyEngineV1 royaltyEngine, - address payable[] memory mockRecipients, - uint256[] memory mockAmounts - ) - { - mockRecipients = new address payable[](2); - mockAmounts = new uint256[](2); - - mockRecipients[0] = payable(address(0x12345)); - mockRecipients[1] = payable(address(0x56789)); - - mockAmounts[0] = 10; - mockAmounts[1] = 15; - - royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); - } - function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 listingId) { // Sample listing parameters. address assetContract = erc721TokenAddress; @@ -196,7 +185,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -230,7 +226,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.prank(buyer); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); console.log("done"); } @@ -247,11 +247,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // 1. ========= Create listing ========= uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); - console.log("here"); + // 2. ========= Buy from listing ========= uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); - console.log("here11"); + // 3. ======== Check balances after royalty payments ======== { @@ -259,62 +259,80 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; assertBalERC20Eq(address(erc20), platformFeeRecipient, platforfee); - console.log("platforfee: %s", platforfee); // Royalty recipient receives correct amounts assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); - console.log("here2"); // Seller gets total price minus royalty amount minus platform fee assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - platforfee); - console.log("here3"); } } - function test_revert_feesExceedTotalPrice() public { - (MockRoyaltyEngineV1 royaltyEngine,,) = _setupRoyaltyEngine(); + function test_revert_mintra_native_royalty_feesExceedTotalPrice() public { + // Set native royalty too high + vm.prank(collectionOwner); + mintraDirectListingsLogicStandalone.createOrUpdateRoyalty(address(erc721Ownable), 10000, factoryAdmin); - // Add RoyaltyEngine to marketplace - vm.prank(marketplaceDeployer); - RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + // 1. ========= Create listing ========= + erc721Ownable.mint(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721Ownable)); - assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + // 2. ========= Buy from listing ========= + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.expectRevert("fees exceed the price"); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + ); + } - // Set platform fee on marketplace - address platformFeeRecipient = marketplaceDeployer; - uint128 platformFeeBps = 10_000; // equal to max bps 10_000 or 100% - vm.prank(marketplaceDeployer); - IPlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, platformFeeBps); + function test_revert_erc2981_royalty_feesExceedTotalPrice() public { + // Set erc2981 royalty too high + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, 10000); // 1. ========= Create listing ========= - - _setupERC721BalanceForSeller(seller, 1); - uint256 listingId = _setupListingForRoyaltyTests(address(erc721)); + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); // 2. ========= Buy from listing ========= - IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); - address buyFor = buyer; uint256 quantityToBuy = listing.quantity; address currency = listing.currency; uint256 pricePerToken = listing.pricePerToken; uint256 totalPrice = pricePerToken * quantityToBuy; - // Mint requisite total price to buyer. erc20.mint(buyer, totalPrice); - // Approve marketplace to transfer currency vm.prank(buyer); erc20.increaseAllowance(marketplace, totalPrice); - // Buy tokens from listing. vm.warp(listing.startTimestamp); - vm.expectRevert("fees exceed the price"); vm.prank(buyer); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); } @@ -346,7 +364,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -402,7 +427,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -434,7 +466,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -466,7 +505,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -498,7 +544,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -534,7 +587,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -566,7 +626,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -587,10 +654,16 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); - vm.prank(seller); vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); } @@ -626,7 +699,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(seller); @@ -779,10 +859,6 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { listingParamsToUpdate.assetContract = address(erc20); // Listing non ERC721 / ERC1155 token. - // Grant ERC20 token asset role. - vm.prank(marketplaceDeployer); - Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc20)); - vm.prank(seller); vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); @@ -832,7 +908,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { //////////////////////////////////////////////////////////////*/ function _setup_cancelListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { - (listingId,) = _setup_updateListing(); + (listingId, ) = _setup_updateListing(); listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); } @@ -846,8 +922,9 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { MintraDirectListingsLogicStandalone(marketplace).cancelListing(listingId); // status should be `CANCELLED` - IDirectListings.Listing memory cancelledListing = - MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + IDirectListings.Listing memory cancelledListing = MintraDirectListingsLogicStandalone(marketplace).getListing( + listingId + ); assertTrue(cancelledListing.status == IDirectListings.Status.CANCELLED); } @@ -879,7 +956,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { //////////////////////////////////////////////////////////////*/ function _setup_approveBuyerForListing() private returns (uint256 listingId) { - (listingId,) = _setup_updateListing(); + (listingId, ) = _setup_updateListing(); } function test_state_approveBuyerForListing() public { @@ -932,7 +1009,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { //////////////////////////////////////////////////////////////*/ function _setup_approveCurrencyForListing() private returns (uint256 listingId) { - (listingId,) = _setup_updateListing(); + (listingId, ) = _setup_updateListing(); } function test_state_approveCurrencyForListing() public { @@ -943,11 +1020,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Seller approves buyer for reserved listing. vm.prank(seller); MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( - listingId, currencyToApprove, pricePerTokenForCurrency + listingId, + currencyToApprove, + pricePerTokenForCurrency ); assertEq( - MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), true + MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + true ); assertEq( MintraDirectListingsLogicStandalone(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), @@ -965,7 +1045,9 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(notSeller); vm.expectRevert("Marketplace: not listing creator."); MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( - listingId, currencyToApprove, pricePerTokenForCurrency + listingId, + currencyToApprove, + pricePerTokenForCurrency ); } @@ -978,7 +1060,9 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(seller); vm.expectRevert("Marketplace: approving listing currency with different price."); MintraDirectListingsLogicStandalone(marketplace).approveCurrencyForListing( - listingId, currencyToApprove, pricePerTokenForCurrency + listingId, + currencyToApprove, + pricePerTokenForCurrency ); } @@ -987,7 +1071,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { //////////////////////////////////////////////////////////////*/ function _setup_buyFromListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { - (listingId,) = _setup_updateListing(); + (listingId, ) = _setup_updateListing(); listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); } @@ -999,6 +1083,8 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { address currency = listing.currency; uint256 pricePerToken = listing.pricePerToken; uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; // Seller approves buyer for listing vm.prank(seller); @@ -1023,7 +1109,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.prank(buyer); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); // Verify that buyer is owner of listed tokens, post-sale. @@ -1032,12 +1122,12 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Verify seller is paid total price. assertBalERC20Eq(address(erc20), buyer, 0); - assertBalERC20Eq(address(erc20), seller, totalPrice); + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFee); if (quantityToBuy == listing.quantity) { // Verify listing status is `COMPLETED` if listing tokens are all bought. - IDirectListings.Listing memory completedListing = - MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + IDirectListings.Listing memory completedListing = MintraDirectListingsLogicStandalone(marketplace) + .getListing(listingId); assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); } } @@ -1050,6 +1140,8 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { address currency = NATIVE_TOKEN; uint256 pricePerToken = listing.pricePerToken; uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; // Approve NATIVE_TOKEN for listing vm.prank(seller); @@ -1073,8 +1165,12 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: totalPrice}( - listingId, buyFor, quantityToBuy, currency, totalPrice + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: totalPrice }( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); // Verify that buyer is owner of listed tokens, post-sale. @@ -1083,12 +1179,12 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Verify seller is paid total price. assertEq(buyer.balance, buyerBalBefore - totalPrice); - assertEq(seller.balance, sellerBalBefore + totalPrice); + assertEq(seller.balance, sellerBalBefore + (totalPrice - platformFee)); if (quantityToBuy == listing.quantity) { // Verify listing status is `COMPLETED` if listing tokens are all bought. - IDirectListings.Listing memory completedListing = - MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + IDirectListings.Listing memory completedListing = MintraDirectListingsLogicStandalone(marketplace) + .getListing(listingId); assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); } } @@ -1123,8 +1219,13 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.prank(buyer); vm.expectRevert("Marketplace: msg.value must exactly be the total price."); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: totalPrice - 1}( // sending insufficient value - listingId, buyFor, quantityToBuy, currency, totalPrice); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: totalPrice - 1 }( // sending insufficient value + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + ); } function test_revert_buyFromListing_unexpectedTotalPrice() public { @@ -1157,7 +1258,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.prank(buyer); vm.expectRevert("Unexpected total price"); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: totalPrice}( + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: totalPrice }( listingId, buyFor, quantityToBuy, @@ -1205,7 +1306,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(buyer); vm.expectRevert("Paying in invalid currency."); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, NATIVE_TOKEN, totalPrice + listingId, + buyFor, + quantityToBuy, + NATIVE_TOKEN, + totalPrice ); } @@ -1353,7 +1458,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(buyer); vm.expectRevert("Buying invalid quantity"); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); } @@ -1385,7 +1494,14 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // List tokens. IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( - assetContract, tokenId, quantity, currency, pricePerToken, startTimestamp, endTimestamp, reserved + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved ); vm.prank(_seller); @@ -1417,8 +1533,12 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { erc20.approve(marketplace, 10 ether); vm.expectRevert("Marketplace: invalid native tokens sent."); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{value: 1 ether}( - listingId, buyer, 1, address(erc20), 1 ether + MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: 1 ether }( + listingId, + buyer, + 1, + address(erc20), + 1 ether ); vm.stopPrank(); diff --git a/src/test/mocks/MockERC721Ownable.sol b/src/test/mocks/MockERC721Ownable.sol new file mode 100644 index 000000000..6bfa7d3c3 --- /dev/null +++ b/src/test/mocks/MockERC721Ownable.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockERC721Ownable is ERC721Burnable, Ownable { + uint256 public nextTokenIdToMint; + + constructor() ERC721("MockERC721Ownable", "MOCK") {} + + function mint(address _receiver, uint256 _amount) external { + uint256 tokenId = nextTokenIdToMint; + nextTokenIdToMint += _amount; + + for (uint256 i = 0; i < _amount; i += 1) { + _mint(_receiver, tokenId); + tokenId += 1; + } + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } +} From ee1311d3ccada2613642eeb5a0ad633f0b086e73 Mon Sep 17 00:00:00 2001 From: hexlive Date: Wed, 29 Nov 2023 16:09:48 -0500 Subject: [PATCH 08/16] Code coverage to 100%. Removed not needed import. Removed indexed attribute for some of the event fields. Added the status object to the newsale event so we can tell more easily if the listing is still active. --- .../MintraDirectListingsLogicStandalone.sol | 16 +- package.json | 1 + .../MintraDirectListingStandalone.t.sol | 402 +++++++++++++++++- 3 files changed, 403 insertions(+), 16 deletions(-) diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 64543d1ce..ae8392e8c 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -1,24 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -/// @author thirdweb +/// @author thirdweb.com / mintra.ai import "./DirectListingsStorage.sol"; // ====== External imports ====== import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; -import "../../../eip/interface/IERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/interfaces/IERC2981.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // ====== Internal imports ====== +import "../../../eip/interface/IERC721.sol"; import "../../../extension/Multicall.sol"; -import "../../../extension/interface/IPlatformFee.sol"; -import "../../../extension/upgradeable/ERC2771ContextConsumer.sol"; import "../../../extension/upgradeable/ReentrancyGuard.sol"; -import "../../../extension/upgradeable/PermissionsEnumerable.sol"; -import { RoyaltyPaymentsLogic } from "../../../extension/upgradeable/RoyaltyPayments.sol"; import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; /** @@ -34,9 +30,10 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } event NewSale( - address indexed listingCreator, - uint256 indexed listingId, - address indexed assetContract, + address listingCreator, + uint256 listingId, + address assetContract, + Status status, uint256 tokenId, address buyer, uint256 quantityBought, @@ -333,6 +330,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen listing.listingCreator, listing.listingId, listing.assetContract, + listing.status, listing.tokenId, buyer, _quantity, diff --git a/package.json b/package.json index 2cc66d2a6..1a8d15413 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "prettier:list-different": "prettier --config .prettierrc --list-different \"**/*.{js,json,sol,ts}\"", "prettier:contracts": "prettier --config .prettierrc --list-different \"{contracts,src}/**/*.sol\"", "test": "forge test", + "coverage_mintra": "forge coverage --match-contract MintraDirectListingsLogicStandaloneTest --report lcov && genhtml lcov.info --output-dir coverage", "typechain": "typechain --target ethers-v5 --out-dir ./typechain artifacts_forge/**/*.json", "build": "yarn clean && yarn compile", "forge:build": "forge build", diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index 375efde7d..b0e793952 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -340,6 +340,72 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Create listing //////////////////////////////////////////////////////////////*/ + function test_state_createListing_1155() public { + // Sample listing parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 2; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + erc1155.mint(seller, tokenId, quantity, ""); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = quantity; + + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + uint256 listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Total listings incremented + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, startTimestamp); + assertEq(listing.endTimestamp, endTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC1155)); + } + function test_state_createListing() public { // Sample listing parameters. address assetContract = address(erc721); @@ -401,6 +467,74 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); } + function test_state_createListing_start_time_in_past() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + + vm.warp(10000); // Set the timestop for block 1 to 10000 + + uint256 expectedStartTimestamp = 10000; + uint256 expectedEndTimestamp = type(uint128).max; + // Set the start time to be at a timestamp in the past + uint128 startTimestamp = uint128(block.timestamp) - 1000; + + uint128 endTimestamp = type(uint128).max; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + uint256 listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings incremented + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, expectedStartTimestamp); + assertEq(listing.endTimestamp, expectedEndTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + function test_revert_createListing_notOwnerOfListedToken() public { // Sample listing parameters. address assetContract = address(erc721); @@ -753,6 +887,54 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); } + function test_state_updateListing_start_time_in_past() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.pricePerToken = 2 ether; + + // Update the start time of the listing + uint256 expectedStartTimestamp = block.timestamp + 10; + uint256 expectedEndTimestamp = type(uint128).max; + + listingParamsToUpdate.startTimestamp = uint128(block.timestamp); + listingParamsToUpdate.endTimestamp = type(uint128).max; + vm.warp(block.timestamp + 10); // Set the timestamp 10 seconds in the future + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).updateListing(listingId, listingParamsToUpdate); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings not incremented on update. + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, listingParamsToUpdate.assetContract); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, listingParamsToUpdate.quantity); + assertEq(listing.currency, listingParamsToUpdate.currency); + assertEq(listing.pricePerToken, listingParamsToUpdate.pricePerToken); + assertEq(listing.startTimestamp, expectedStartTimestamp); + assertEq(listing.endTimestamp, expectedEndTimestamp); + assertEq(listing.reserved, listingParamsToUpdate.reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + function test_revert_updateListing_notListingCreator() public { (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); @@ -1075,7 +1257,61 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); } - function test_state_buyFromListing() public { + function test_state_buyFromListing_with_mint_token() public { + uint256 listingId = _createListing(seller, address(erc20Aux)); + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBpsMint = MintraDirectListingsLogicStandalone(marketplace).platformFeeBpsMint(); + uint256 platformFee = (totalPrice * platformFeeBpsMint) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20Aux.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20Aux), buyer, totalPrice); + assertBalERC20Eq(address(erc20Aux), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20Aux.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + ); + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20Aux), buyer, 0); + assertBalERC20Eq(address(erc20Aux), seller, totalPrice - platformFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = MintraDirectListingsLogicStandalone(marketplace) + .getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_state_buyFromListing_721() public { (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); address buyFor = buyer; @@ -1132,6 +1368,69 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } + function test_state_buyFromListing_1155() public { + // Create the listing + test_state_createListing_1155(); + + //(uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + uint256 listingId = 0; + + IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 tokenId = listing.tokenId; + uint256 quantity = listing.quantity; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = quantity; + + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice + ); + + // Verify that buyer is owner of listed tokens, post-sale. + assertBalERC1155Eq(address(erc1155), buyer, tokenIds, amounts); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = MintraDirectListingsLogicStandalone(marketplace) + .getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + function test_state_buyFromListing_nativeToken() public { (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); @@ -1347,7 +1646,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(buyer); vm.expectRevert("!BAL20"); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); } @@ -1384,7 +1687,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(buyer); vm.expectRevert("!BAL20"); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); } @@ -1421,7 +1728,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.prank(buyer); vm.expectRevert("Buying invalid quantity"); MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, buyFor, quantityToBuy, currency, totalPrice + listingId, + buyFor, + quantityToBuy, + currency, + totalPrice ); } @@ -1469,17 +1780,75 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { /*/////////////////////////////////////////////////////////////// View functions //////////////////////////////////////////////////////////////*/ + function test_getAllListing() public { + // Create the listing + test_state_createListing_1155(); + + IDirectListings.Listing[] memory listings = MintraDirectListingsLogicStandalone(marketplace).getAllListings( + 0, + 0 + ); + + assertEq(listings.length, 1); + + IDirectListings.Listing memory listing = listings[0]; + + assertEq(listing.assetContract, address(erc1155)); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, 2); + assertEq(listing.currency, address(erc20)); + assertEq(listing.pricePerToken, 1 ether); + assertEq(listing.startTimestamp, 100); + assertEq(listing.endTimestamp, 200); + assertEq(listing.reserved, false); + } + + function test_getAllValidListings() public { + // Create the listing + test_state_createListing_1155(); + + IDirectListings.Listing[] memory listingsAll = MintraDirectListingsLogicStandalone(marketplace).getAllListings( + 0, + 0 + ); + + assertEq(listingsAll.length, 1); + + vm.warp(listingsAll[0].startTimestamp); + IDirectListings.Listing[] memory listings = MintraDirectListingsLogicStandalone(marketplace) + .getAllValidListings(0, 0); + + assertEq(listings.length, 1); + + IDirectListings.Listing memory listing = listings[0]; - function _createListing(address _seller) private returns (uint256 listingId) { + assertEq(listing.assetContract, address(erc1155)); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, 2); + assertEq(listing.currency, address(erc20)); + assertEq(listing.pricePerToken, 1 ether); + assertEq(listing.startTimestamp, 100); + assertEq(listing.endTimestamp, 200); + assertEq(listing.reserved, false); + } + + function test_currencyPriceForListing_fail() public { + // Create the listing + test_state_createListing_1155(); + + vm.expectRevert("Currency not approved for listing"); + MintraDirectListingsLogicStandalone(marketplace).currencyPriceForListing(0, address(erc20Aux)); + } + + function _createListing(address _seller, address currency) private returns (uint256 listingId) { // Sample listing parameters. address assetContract = address(erc721); uint256 tokenId = 0; uint256 quantity = 1; - address currency = address(erc20); uint256 pricePerToken = 1 ether; uint128 startTimestamp = 100; uint128 endTimestamp = 200; - bool reserved = true; + bool reserved = false; // Mint the ERC721 tokens to seller. These tokens will be listed. _setupERC721BalanceForSeller(_seller, 1); @@ -1545,4 +1914,23 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // 1 ether is temporary locked in contract assertEq(marketplace.balance, 0 ether); } + + function test_set_platform_fee() public { + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + assertEq(platformFeeBps, 225); + + vm.prank(wizard); + MintraDirectListingsLogicStandalone(marketplace).setPlatformFeeBps(369); + + platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + + console.log("platformFeeBps", platformFeeBps); + assertEq(platformFeeBps, 369); + } + + function test_set_platform_fee_fail() public { + vm.prank(wizard); + vm.expectRevert("Fee not in range"); + MintraDirectListingsLogicStandalone(marketplace).setPlatformFeeBps(1000); + } } From 5fb9cbbf189ce69b368e9edb83c900dd483281a5 Mon Sep 17 00:00:00 2001 From: hexlive Date: Fri, 1 Dec 2023 13:10:16 -0500 Subject: [PATCH 09/16] Updated Events. Changed the Names of overridden events because the abi generate is having issues with with overridden events --- .../MintraDirectListingsLogicStandalone.sol | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index ae8392e8c..b7bcafc8e 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -29,19 +29,15 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 basisPoints; } - event NewSale( - address listingCreator, + event MintaNewSale( uint256 listingId, - address assetContract, - Status status, - uint256 tokenId, address buyer, uint256 quantityBought, uint256 totalPricePaid, address currency ); - event RoyaltyTransfered( + event MintraRoyaltyTransfered( address assetContract, uint256 tokenId, uint256 listingId, @@ -326,12 +322,8 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); - emit NewSale( - listing.listingCreator, + emit MintaNewSale( listing.listingId, - listing.assetContract, - listing.status, - listing.tokenId, buyer, _quantity, targetTotalPrice, @@ -616,7 +608,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen amountRemaining = amountRemaining - royaltyAmount; - emit RoyaltyTransfered( + emit MintraRoyaltyTransfered( _listing.assetContract, _listing.tokenId, _listing.listingId, From 8e35ee2ce4cf35825231933b91cc2f1b963a4a67 Mon Sep 17 00:00:00 2001 From: hexlive Date: Mon, 4 Dec 2023 11:05:57 -0500 Subject: [PATCH 10/16] Fix spelling error on event name --- .../MintraDirectListingsLogicStandalone.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index b7bcafc8e..1a5a59166 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -29,7 +29,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 basisPoints; } - event MintaNewSale( + event MintraNewSale( uint256 listingId, address buyer, uint256 quantityBought, @@ -322,13 +322,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); - emit MintaNewSale( - listing.listingId, - buyer, - _quantity, - targetTotalPrice, - _currency - ); + emit MintraNewSale(listing.listingId, buyer, _quantity, targetTotalPrice, _currency); } /*/////////////////////////////////////////////////////////////// From 4761aecc5591bf1d2b83d31297f149f7eb798f52 Mon Sep 17 00:00:00 2001 From: hexlive Date: Tue, 26 Dec 2023 11:36:41 -0500 Subject: [PATCH 11/16] Added bulk but method. --- .../prebuilts/marketplace/IMarketplace.sol | 17 ---- .../MintraDirectListingsLogicStandalone.sol | 98 ++++++++++++------- 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/contracts/prebuilts/marketplace/IMarketplace.sol b/contracts/prebuilts/marketplace/IMarketplace.sol index 0b9e05bcf..71889ee07 100644 --- a/contracts/prebuilts/marketplace/IMarketplace.sol +++ b/contracts/prebuilts/marketplace/IMarketplace.sol @@ -159,23 +159,6 @@ interface IDirectListings { uint256 _pricePerTokenInCurrency ) external; - /** - * @notice Buy NFTs from a listing. - * - * @param _listingId The ID of the listing to update. - * @param _buyFor The recipient of the NFTs being bought. - * @param _quantity The quantity of NFTs to buy from the listing. - * @param _currency The currency to use to pay for NFTs. - * @param _expectedTotalPrice The expected total price to pay for the NFTs being bought. - */ - function buyFromListing( - uint256 _listingId, - address _buyFor, - uint256 _quantity, - address _currency, - uint256 _expectedTotalPrice - ) external payable; - /** * @notice Returns the total number of listings created. * @dev At any point, the return value is the ID of the next listing created. diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 1a5a59166..35d6ba2bc 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -157,11 +157,10 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } /// @notice Update parameters of a listing of NFTs. - function updateListing(uint256 _listingId, ListingParameters memory _params) - external - onlyExistingListing(_listingId) - onlyListingCreator(_listingId) - { + function updateListing( + uint256 _listingId, + ListingParameters memory _params + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { address listingCreator = msg.sender; Listing memory listing = _directListingsStorage().listings[_listingId]; TokenType tokenType = _getTokenType(_params.assetContract); @@ -263,19 +262,58 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen emit CurrencyApprovedForListing(_listingId, _currency, _pricePerTokenInCurrency); } + function bulkBuyFromListing( + uint256[] memory _listingId, + address[] memory _buyFor, + uint256[] memory _quantity, + address[] memory _currency, + uint256[] memory _expectedTotalPrice + ) external payable nonReentrant { + uint256 totalAmountPls = 0; + // Iterate over each tokenId + for (uint256 i = 0; i < _listingId.length; i++) { + // Are we buying this item in PLS + uint256 price; + + Listing memory listing = _directListingsStorage().listings[_listingId[i]]; + + require(listing.status == IDirectListings.Status.CREATED, "Marketplace: invalid listing."); + + if (_currency[i] == CurrencyTransferLib.NATIVE_TOKEN) { + //calculate total amount for items being sold for PLS + if (_directListingsStorage().currencyPriceForListing[_listingId[i]][_currency[i]] > 0) { + price = + _quantity[i] * + _directListingsStorage().currencyPriceForListing[_listingId[i]][_currency[i]]; + } else { + require(_currency[i] == listing.currency, "Paying in invalid currency."); + price = _quantity[i] * listing.pricePerToken; + } + + totalAmountPls += price; + } + + // Call the buy function for the current tokenId + _buyFromListing(listing, _buyFor[i], _quantity[i], _currency[i], _expectedTotalPrice[i]); + } + + // Make sure that the total price for items bought with PLS is equal to the amount sent + require(msg.value == totalAmountPls || (totalAmountPls == 0 && msg.value == 0), "Incorrect"); + } + /// @notice Buy NFTs from a listing. - function buyFromListing( - uint256 _listingId, + function _buyFromListing( + Listing memory listing, address _buyFor, uint256 _quantity, address _currency, uint256 _expectedTotalPrice - ) external payable nonReentrant onlyExistingListing(_listingId) { - Listing memory listing = _directListingsStorage().listings[_listingId]; + ) internal { + uint256 listingId = listing.listingId; address buyer = msg.sender; require( - !listing.reserved || _directListingsStorage().isBuyerApprovedForListing[_listingId][buyer], + !listing.reserved || _directListingsStorage().isBuyerApprovedForListing[listingId][buyer], "buyer not approved" ); require(_quantity > 0 && _quantity <= listing.quantity, "Buying invalid quantity"); @@ -297,27 +335,27 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 targetTotalPrice; - if (_directListingsStorage().currencyPriceForListing[_listingId][_currency] > 0) { - targetTotalPrice = _quantity * _directListingsStorage().currencyPriceForListing[_listingId][_currency]; + // Check: is the buyer paying in a currency that the listing creator approved + if (_directListingsStorage().currencyPriceForListing[listingId][_currency] > 0) { + targetTotalPrice = _quantity * _directListingsStorage().currencyPriceForListing[listingId][_currency]; } else { require(_currency == listing.currency, "Paying in invalid currency."); targetTotalPrice = _quantity * listing.pricePerToken; } + // Check: is the buyer paying the price that the buyer is expecting to pay. + // This is to prevent attack where the seller could change the price + // right before the buyers tranaction executes. require(targetTotalPrice == _expectedTotalPrice, "Unexpected total price"); - // Check: buyer owns and has approved sufficient currency for sale. - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == targetTotalPrice, "Marketplace: msg.value must exactly be the total price."); - } else { - require(msg.value == 0, "Marketplace: invalid native tokens sent."); + if (_currency != CurrencyTransferLib.NATIVE_TOKEN) { _validateERC20BalAndAllowance(buyer, _currency, targetTotalPrice); } if (listing.quantity == _quantity) { - _directListingsStorage().listings[_listingId].status = IDirectListings.Status.COMPLETED; + _directListingsStorage().listings[listingId].status = IDirectListings.Status.COMPLETED; } - _directListingsStorage().listings[_listingId].quantity -= _quantity; + _directListingsStorage().listings[listingId].quantity -= _quantity; _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); @@ -372,11 +410,10 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen * A valid listing is where the listing creator still owns and has approved Marketplace * to transfer the listed NFTs. */ - function getAllValidListings(uint256 _startId, uint256 _endId) - external - view - returns (Listing[] memory _validListings) - { + function getAllValidListings( + uint256 _startId, + uint256 _endId + ) external view returns (Listing[] memory _validListings) { require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); Listing[] memory _listings = new Listing[](_endId - _startId + 1); @@ -519,11 +556,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } /// @dev Validates that `_tokenOwner` owns and has approved Markeplace to transfer the appropriate amount of currency - function _validateERC20BalAndAllowance( - address _tokenOwner, - address _currency, - uint256 _amount - ) internal view { + function _validateERC20BalAndAllowance(address _tokenOwner, address _currency, uint256 _amount) internal view { require( IERC20(_currency).balanceOf(_tokenOwner) >= _amount && IERC20(_currency).allowance(_tokenOwner, address(this)) >= _amount, @@ -532,12 +565,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } /// @dev Transfers tokens listed for sale in a direct or auction listing. - function _transferListingTokens( - address _from, - address _to, - uint256 _quantity, - Listing memory _listing - ) internal { + function _transferListingTokens(address _from, address _to, uint256 _quantity, Listing memory _listing) internal { if (_listing.tokenType == TokenType.ERC1155) { IERC1155(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, _quantity, ""); } else if (_listing.tokenType == TokenType.ERC721) { From 35cf993b8bff452f91197c1f42cf4f9a3bc352dc Mon Sep 17 00:00:00 2001 From: hexlive Date: Sat, 30 Dec 2023 23:26:20 -0500 Subject: [PATCH 12/16] Update how funds are transferred. Start getting the tests to pass for the bulk function updates --- .../MintraDirectListingsLogicStandalone.sol | 28 +- .../MintraDirectListingStandalone.t.sol | 750 +++++++++++------- 2 files changed, 452 insertions(+), 326 deletions(-) diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol index 35d6ba2bc..fc65ac417 100644 --- a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListingsLogicStandalone.sol @@ -298,7 +298,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } // Make sure that the total price for items bought with PLS is equal to the amount sent - require(msg.value == totalAmountPls || (totalAmountPls == 0 && msg.value == 0), "Incorrect"); + require(msg.value == totalAmountPls || (totalAmountPls == 0 && msg.value == 0), "Incorrect PLS amount sent"); } /// @notice Buy NFTs from a listing. @@ -358,6 +358,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen _directListingsStorage().listings[listingId].quantity -= _quantity; _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); + _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); emit MintraNewSale(listing.listingId, buyer, _quantity, targetTotalPrice, _currency); @@ -581,7 +582,6 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen uint256 _totalPayoutAmount, Listing memory _listing ) internal { - address _nativeTokenWrapper = nativeTokenWrapper; uint256 amountRemaining; uint256 platformFeeCut; @@ -595,13 +595,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } // Transfer platform fee - CurrencyTransferLib.transferCurrencyWithWrapper( - _currencyToUse, - _payer, - platformFeeRecipient, - platformFeeCut, - _nativeTokenWrapper - ); + CurrencyTransferLib.transferCurrency(_currencyToUse, _payer, platformFeeRecipient, platformFeeCut); amountRemaining = _totalPayoutAmount - platformFeeCut; } @@ -620,13 +614,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen require(amountRemaining >= royaltyAmount, "fees exceed the price"); // Transfer royalty - CurrencyTransferLib.transferCurrencyWithWrapper( - _currencyToUse, - _payer, - royaltyRecipient, - royaltyAmount, - _nativeTokenWrapper - ); + CurrencyTransferLib.transferCurrency(_currencyToUse, _payer, royaltyRecipient, royaltyAmount); amountRemaining = amountRemaining - royaltyAmount; @@ -644,13 +632,7 @@ contract MintraDirectListingsLogicStandalone is IDirectListings, Multicall, Reen } // Distribute price to token owner - CurrencyTransferLib.transferCurrencyWithWrapper( - _currencyToUse, - _payer, - _payee, - amountRemaining, - _nativeTokenWrapper - ); + CurrencyTransferLib.transferCurrency(_currencyToUse, _payer, _payee, amountRemaining); } function processRoyalty( diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index b0e793952..d7f0b69fb 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -214,25 +214,33 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Approve marketplace to transfer currency vm.prank(buyer); erc20.increaseAllowance(marketplace, totalPrice); - uint256 bla = erc20.allowance(buyer, marketplace); - - console.log(totalPrice); - console.log("bla1"); - console.log(bla); - console.log(listing.currency); - console.log(address(erc20)); // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray ); - console.log("done"); } function test_noRoyaltyEngine_defaultERC2981Token() public { @@ -293,12 +301,28 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.expectRevert("fees exceed the price"); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray ); } @@ -327,12 +351,28 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.expectRevert("fees exceed the price"); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray ); } @@ -1287,13 +1327,31 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); + + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } // Verify that buyer is owner of listed tokens, post-sale. assertIsOwnerERC721(address(erc721), buyer, tokenIds); @@ -1344,14 +1402,31 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } // Verify that buyer is owner of listed tokens, post-sale. assertIsOwnerERC721(address(erc721), buyer, tokenIds); assertIsNotOwnerERC721(address(erc721), seller, tokenIds); @@ -1408,13 +1483,30 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } // Verify that buyer is owner of listed tokens, post-sale. assertBalERC1155Eq(address(erc1155), buyer, tokenIds, amounts); @@ -1431,7 +1523,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } - function test_state_buyFromListing_nativeToken() public { + function test_state_bulkBuyFromListing_nativeToken() public { (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); address buyFor = buyer; @@ -1464,13 +1556,30 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: totalPrice }( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing{ value: totalPrice }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } // Verify that buyer is owner of listed tokens, post-sale. assertIsOwnerERC721(address(erc721), buyer, tokenIds); @@ -1488,7 +1597,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } - function test_revert_buyFromListing_nativeToken_incorrectValueSent() public { + function test_revert_bulkBuyFromListing_nativeToken_incorrectValueSent() public { (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); address buyFor = buyer; @@ -1517,14 +1626,31 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { // Buy tokens from listing. vm.warp(listing.startTimestamp); vm.prank(buyer); - vm.expectRevert("Marketplace: msg.value must exactly be the total price."); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: totalPrice - 1 }( // sending insufficient value - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); + vm.expectRevert("native token transfer failed"); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing{ value: totalPrice - 1 }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } } function test_revert_buyFromListing_unexpectedTotalPrice() public { @@ -1557,225 +1683,243 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { vm.warp(listing.startTimestamp); vm.prank(buyer); vm.expectRevert("Unexpected total price"); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: totalPrice }( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice + 1 // Pass unexpected total price - ); - } - - function test_revert_buyFromListing_invalidCurrency() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - address buyFor = buyer; - uint256 quantityToBuy = listing.quantity; - uint256 pricePerToken = listing.pricePerToken; - uint256 totalPrice = pricePerToken * quantityToBuy; - // Seller approves buyer for listing - vm.prank(seller); - MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // Verify that seller is owner of listed tokens, pre-sale. - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - assertIsOwnerERC721(address(erc721), seller, tokenIds); - assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // Mint requisite total price to buyer. - erc20.mint(buyer, totalPrice); - assertBalERC20Eq(address(erc20), buyer, totalPrice); - assertBalERC20Eq(address(erc20), seller, 0); - - // Approve marketplace to transfer currency - vm.prank(buyer); - erc20.increaseAllowance(marketplace, totalPrice); - - // Buy tokens from listing. - - assertEq(listing.currency, address(erc20)); - assertEq( - MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), - false - ); - - vm.warp(listing.startTimestamp); - vm.prank(buyer); - vm.expectRevert("Paying in invalid currency."); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - NATIVE_TOKEN, - totalPrice - ); - } - - function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - address buyFor = buyer; - uint256 quantityToBuy = listing.quantity; - address currency = listing.currency; - uint256 pricePerToken = listing.pricePerToken; - uint256 totalPrice = pricePerToken * quantityToBuy; - - // Seller approves buyer for listing - vm.prank(seller); - MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // Verify that seller is owner of listed tokens, pre-sale. - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - assertIsOwnerERC721(address(erc721), seller, tokenIds); - assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // Mint requisite total price to buyer. - erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price - assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); - assertBalERC20Eq(address(erc20), seller, 0); - - // Approve marketplace to transfer currency - vm.prank(buyer); - erc20.increaseAllowance(marketplace, totalPrice); - - // Buy tokens from listing. - vm.warp(listing.startTimestamp); - vm.prank(buyer); - vm.expectRevert("!BAL20"); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); - } - - function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - address buyFor = buyer; - uint256 quantityToBuy = listing.quantity; - address currency = listing.currency; - uint256 pricePerToken = listing.pricePerToken; - uint256 totalPrice = pricePerToken * quantityToBuy; - - // Seller approves buyer for listing - vm.prank(seller); - MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // Verify that seller is owner of listed tokens, pre-sale. - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - assertIsOwnerERC721(address(erc721), seller, tokenIds); - assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // Mint requisite total price to buyer. - erc20.mint(buyer, totalPrice); - assertBalERC20Eq(address(erc20), buyer, totalPrice); - assertBalERC20Eq(address(erc20), seller, 0); - - // Don't approve marketplace to transfer currency - vm.prank(buyer); - erc20.approve(marketplace, 0); - - // Buy tokens from listing. - vm.warp(listing.startTimestamp); - vm.prank(buyer); - vm.expectRevert("!BAL20"); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); - } - - function test_revert_buyFromListing_buyingZeroQuantity() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - address buyFor = buyer; - uint256 quantityToBuy = 0; // Buying zero quantity - address currency = listing.currency; - uint256 pricePerToken = listing.pricePerToken; - uint256 totalPrice = pricePerToken * quantityToBuy; + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; - // Seller approves buyer for listing - vm.prank(seller); - MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; - // Verify that seller is owner of listed tokens, pre-sale. - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - assertIsOwnerERC721(address(erc721), seller, tokenIds); - assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; - // Mint requisite total price to buyer. - erc20.mint(buyer, totalPrice); - assertBalERC20Eq(address(erc20), buyer, totalPrice); - assertBalERC20Eq(address(erc20), seller, 0); + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; - // Don't approve marketplace to transfer currency - vm.prank(buyer); - erc20.increaseAllowance(marketplace, totalPrice); + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice + 1; - // Buy tokens from listing. - vm.warp(listing.startTimestamp); - vm.prank(buyer); - vm.expectRevert("Buying invalid quantity"); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing{ value: totalPrice - 1 }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } } - function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - address buyFor = buyer; - uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. - address currency = listing.currency; - uint256 pricePerToken = listing.pricePerToken; - uint256 totalPrice = pricePerToken * quantityToBuy; - - // Seller approves buyer for listing - vm.prank(seller); - MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // Verify that seller is owner of listed tokens, pre-sale. - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 0; - assertIsOwnerERC721(address(erc721), seller, tokenIds); - assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // Mint requisite total price to buyer. - erc20.mint(buyer, totalPrice); - assertBalERC20Eq(address(erc20), buyer, totalPrice); - assertBalERC20Eq(address(erc20), seller, 0); - - // Don't approve marketplace to transfer currency - vm.prank(buyer); - erc20.increaseAllowance(marketplace, totalPrice); - - // Buy tokens from listing. - vm.warp(listing.startTimestamp); - vm.prank(buyer); - vm.expectRevert("Buying invalid quantity"); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - listingId, - buyFor, - quantityToBuy, - currency, - totalPrice - ); - } + // function test_revert_buyFromListing_invalidCurrency() public { + // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + // address buyFor = buyer; + // uint256 quantityToBuy = listing.quantity; + // uint256 pricePerToken = listing.pricePerToken; + // uint256 totalPrice = pricePerToken * quantityToBuy; + + // // Seller approves buyer for listing + // vm.prank(seller); + // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // // Verify that seller is owner of listed tokens, pre-sale. + // uint256[] memory tokenIds = new uint256[](1); + // tokenIds[0] = 0; + // assertIsOwnerERC721(address(erc721), seller, tokenIds); + // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // // Mint requisite total price to buyer. + // erc20.mint(buyer, totalPrice); + // assertBalERC20Eq(address(erc20), buyer, totalPrice); + // assertBalERC20Eq(address(erc20), seller, 0); + + // // Approve marketplace to transfer currency + // vm.prank(buyer); + // erc20.increaseAllowance(marketplace, totalPrice); + + // // Buy tokens from listing. + + // assertEq(listing.currency, address(erc20)); + // assertEq( + // MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + // false + // ); + + // vm.warp(listing.startTimestamp); + // vm.prank(buyer); + // vm.expectRevert("Paying in invalid currency."); + // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + // listingId, + // buyFor, + // quantityToBuy, + // NATIVE_TOKEN, + // totalPrice + // ); + // } + + // function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { + // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + // address buyFor = buyer; + // uint256 quantityToBuy = listing.quantity; + // address currency = listing.currency; + // uint256 pricePerToken = listing.pricePerToken; + // uint256 totalPrice = pricePerToken * quantityToBuy; + + // // Seller approves buyer for listing + // vm.prank(seller); + // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // // Verify that seller is owner of listed tokens, pre-sale. + // uint256[] memory tokenIds = new uint256[](1); + // tokenIds[0] = 0; + // assertIsOwnerERC721(address(erc721), seller, tokenIds); + // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // // Mint requisite total price to buyer. + // erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price + // assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); + // assertBalERC20Eq(address(erc20), seller, 0); + + // // Approve marketplace to transfer currency + // vm.prank(buyer); + // erc20.increaseAllowance(marketplace, totalPrice); + + // // Buy tokens from listing. + // vm.warp(listing.startTimestamp); + // vm.prank(buyer); + // vm.expectRevert("!BAL20"); + // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + // listingId, + // buyFor, + // quantityToBuy, + // currency, + // totalPrice + // ); + // } + + // function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { + // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + // address buyFor = buyer; + // uint256 quantityToBuy = listing.quantity; + // address currency = listing.currency; + // uint256 pricePerToken = listing.pricePerToken; + // uint256 totalPrice = pricePerToken * quantityToBuy; + + // // Seller approves buyer for listing + // vm.prank(seller); + // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // // Verify that seller is owner of listed tokens, pre-sale. + // uint256[] memory tokenIds = new uint256[](1); + // tokenIds[0] = 0; + // assertIsOwnerERC721(address(erc721), seller, tokenIds); + // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // // Mint requisite total price to buyer. + // erc20.mint(buyer, totalPrice); + // assertBalERC20Eq(address(erc20), buyer, totalPrice); + // assertBalERC20Eq(address(erc20), seller, 0); + + // // Don't approve marketplace to transfer currency + // vm.prank(buyer); + // erc20.approve(marketplace, 0); + + // // Buy tokens from listing. + // vm.warp(listing.startTimestamp); + // vm.prank(buyer); + // vm.expectRevert("!BAL20"); + // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + // listingId, + // buyFor, + // quantityToBuy, + // currency, + // totalPrice + // ); + // } + + // function test_revert_buyFromListing_buyingZeroQuantity() public { + // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + // address buyFor = buyer; + // uint256 quantityToBuy = 0; // Buying zero quantity + // address currency = listing.currency; + // uint256 pricePerToken = listing.pricePerToken; + // uint256 totalPrice = pricePerToken * quantityToBuy; + + // // Seller approves buyer for listing + // vm.prank(seller); + // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // // Verify that seller is owner of listed tokens, pre-sale. + // uint256[] memory tokenIds = new uint256[](1); + // tokenIds[0] = 0; + // assertIsOwnerERC721(address(erc721), seller, tokenIds); + // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // // Mint requisite total price to buyer. + // erc20.mint(buyer, totalPrice); + // assertBalERC20Eq(address(erc20), buyer, totalPrice); + // assertBalERC20Eq(address(erc20), seller, 0); + + // // Don't approve marketplace to transfer currency + // vm.prank(buyer); + // erc20.increaseAllowance(marketplace, totalPrice); + + // // Buy tokens from listing. + // vm.warp(listing.startTimestamp); + // vm.prank(buyer); + // vm.expectRevert("Buying invalid quantity"); + // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + // listingId, + // buyFor, + // quantityToBuy, + // currency, + // totalPrice + // ); + // } + + // function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { + // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + // address buyFor = buyer; + // uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. + // address currency = listing.currency; + // uint256 pricePerToken = listing.pricePerToken; + // uint256 totalPrice = pricePerToken * quantityToBuy; + + // // Seller approves buyer for listing + // vm.prank(seller); + // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // // Verify that seller is owner of listed tokens, pre-sale. + // uint256[] memory tokenIds = new uint256[](1); + // tokenIds[0] = 0; + // assertIsOwnerERC721(address(erc721), seller, tokenIds); + // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // // Mint requisite total price to buyer. + // erc20.mint(buyer, totalPrice); + // assertBalERC20Eq(address(erc20), buyer, totalPrice); + // assertBalERC20Eq(address(erc20), seller, 0); + + // // Don't approve marketplace to transfer currency + // vm.prank(buyer); + // erc20.increaseAllowance(marketplace, totalPrice); + + // // Buy tokens from listing. + // vm.warp(listing.startTimestamp); + // vm.prank(buyer); + // vm.expectRevert("Buying invalid quantity"); + // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( + // listingId, + // buyFor, + // quantityToBuy, + // currency, + // totalPrice + // ); + // } /*/////////////////////////////////////////////////////////////// View functions @@ -1877,43 +2021,43 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); } - function test_audit_native_tokens_locked() public { - (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); + // function test_audit_native_tokens_locked() public { + // (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = existingListing.tokenId; + // uint256[] memory tokenIds = new uint256[](1); + // tokenIds[0] = existingListing.tokenId; - // Verify existing auction at `auctionId` - assertEq(existingListing.assetContract, address(erc721)); + // // Verify existing auction at `auctionId` + // assertEq(existingListing.assetContract, address(erc721)); - vm.warp(existingListing.startTimestamp); + // vm.warp(existingListing.startTimestamp); - // No ether is locked in contract - assertEq(marketplace.balance, 0); + // // No ether is locked in contract + // assertEq(marketplace.balance, 0); - // buy from listing - erc20.mint(buyer, 10 ether); - vm.deal(buyer, 1 ether); + // // buy from listing + // erc20.mint(buyer, 10 ether); + // vm.deal(buyer, 1 ether); - vm.prank(seller); - MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + // vm.prank(seller); + // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - vm.startPrank(buyer); - erc20.approve(marketplace, 10 ether); + // vm.startPrank(buyer); + // erc20.approve(marketplace, 10 ether); - vm.expectRevert("Marketplace: invalid native tokens sent."); - MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: 1 ether }( - listingId, - buyer, - 1, - address(erc20), - 1 ether - ); - vm.stopPrank(); + // vm.expectRevert("Marketplace: invalid native tokens sent."); + // MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: 1 ether }( + // listingId, + // buyer, + // 1, + // address(erc20), + // 1 ether + // ); + // vm.stopPrank(); - // 1 ether is temporary locked in contract - assertEq(marketplace.balance, 0 ether); - } + // // 1 ether is temporary locked in contract + // assertEq(marketplace.balance, 0 ether); + // } function test_set_platform_fee() public { uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); From 4032d7b64b34eb02388c784cb0b43674a2b3f10f Mon Sep 17 00:00:00 2001 From: hexlive Date: Sun, 31 Dec 2023 21:36:54 -0500 Subject: [PATCH 13/16] Completed updating all tests to use the bulk buy function --- .../MintraDirectListingStandalone.t.sol | 573 ++++++++++-------- 1 file changed, 335 insertions(+), 238 deletions(-) diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index d7f0b69fb..dd1eee0e1 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -1710,216 +1710,296 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } - // function test_revert_buyFromListing_invalidCurrency() public { - // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - // address buyFor = buyer; - // uint256 quantityToBuy = listing.quantity; - // uint256 pricePerToken = listing.pricePerToken; - // uint256 totalPrice = pricePerToken * quantityToBuy; - - // // Seller approves buyer for listing - // vm.prank(seller); - // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // // Verify that seller is owner of listed tokens, pre-sale. - // uint256[] memory tokenIds = new uint256[](1); - // tokenIds[0] = 0; - // assertIsOwnerERC721(address(erc721), seller, tokenIds); - // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // // Mint requisite total price to buyer. - // erc20.mint(buyer, totalPrice); - // assertBalERC20Eq(address(erc20), buyer, totalPrice); - // assertBalERC20Eq(address(erc20), seller, 0); - - // // Approve marketplace to transfer currency - // vm.prank(buyer); - // erc20.increaseAllowance(marketplace, totalPrice); - - // // Buy tokens from listing. - - // assertEq(listing.currency, address(erc20)); - // assertEq( - // MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), - // false - // ); - - // vm.warp(listing.startTimestamp); - // vm.prank(buyer); - // vm.expectRevert("Paying in invalid currency."); - // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - // listingId, - // buyFor, - // quantityToBuy, - // NATIVE_TOKEN, - // totalPrice - // ); - // } - - // function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { - // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - // address buyFor = buyer; - // uint256 quantityToBuy = listing.quantity; - // address currency = listing.currency; - // uint256 pricePerToken = listing.pricePerToken; - // uint256 totalPrice = pricePerToken * quantityToBuy; - - // // Seller approves buyer for listing - // vm.prank(seller); - // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // // Verify that seller is owner of listed tokens, pre-sale. - // uint256[] memory tokenIds = new uint256[](1); - // tokenIds[0] = 0; - // assertIsOwnerERC721(address(erc721), seller, tokenIds); - // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // // Mint requisite total price to buyer. - // erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price - // assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); - // assertBalERC20Eq(address(erc20), seller, 0); - - // // Approve marketplace to transfer currency - // vm.prank(buyer); - // erc20.increaseAllowance(marketplace, totalPrice); - - // // Buy tokens from listing. - // vm.warp(listing.startTimestamp); - // vm.prank(buyer); - // vm.expectRevert("!BAL20"); - // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - // listingId, - // buyFor, - // quantityToBuy, - // currency, - // totalPrice - // ); - // } - - // function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { - // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - // address buyFor = buyer; - // uint256 quantityToBuy = listing.quantity; - // address currency = listing.currency; - // uint256 pricePerToken = listing.pricePerToken; - // uint256 totalPrice = pricePerToken * quantityToBuy; - - // // Seller approves buyer for listing - // vm.prank(seller); - // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // // Verify that seller is owner of listed tokens, pre-sale. - // uint256[] memory tokenIds = new uint256[](1); - // tokenIds[0] = 0; - // assertIsOwnerERC721(address(erc721), seller, tokenIds); - // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // // Mint requisite total price to buyer. - // erc20.mint(buyer, totalPrice); - // assertBalERC20Eq(address(erc20), buyer, totalPrice); - // assertBalERC20Eq(address(erc20), seller, 0); - - // // Don't approve marketplace to transfer currency - // vm.prank(buyer); - // erc20.approve(marketplace, 0); - - // // Buy tokens from listing. - // vm.warp(listing.startTimestamp); - // vm.prank(buyer); - // vm.expectRevert("!BAL20"); - // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - // listingId, - // buyFor, - // quantityToBuy, - // currency, - // totalPrice - // ); - // } - - // function test_revert_buyFromListing_buyingZeroQuantity() public { - // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - // address buyFor = buyer; - // uint256 quantityToBuy = 0; // Buying zero quantity - // address currency = listing.currency; - // uint256 pricePerToken = listing.pricePerToken; - // uint256 totalPrice = pricePerToken * quantityToBuy; - - // // Seller approves buyer for listing - // vm.prank(seller); - // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // // Verify that seller is owner of listed tokens, pre-sale. - // uint256[] memory tokenIds = new uint256[](1); - // tokenIds[0] = 0; - // assertIsOwnerERC721(address(erc721), seller, tokenIds); - // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // // Mint requisite total price to buyer. - // erc20.mint(buyer, totalPrice); - // assertBalERC20Eq(address(erc20), buyer, totalPrice); - // assertBalERC20Eq(address(erc20), seller, 0); - - // // Don't approve marketplace to transfer currency - // vm.prank(buyer); - // erc20.increaseAllowance(marketplace, totalPrice); - - // // Buy tokens from listing. - // vm.warp(listing.startTimestamp); - // vm.prank(buyer); - // vm.expectRevert("Buying invalid quantity"); - // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - // listingId, - // buyFor, - // quantityToBuy, - // currency, - // totalPrice - // ); - // } - - // function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { - // (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - - // address buyFor = buyer; - // uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. - // address currency = listing.currency; - // uint256 pricePerToken = listing.pricePerToken; - // uint256 totalPrice = pricePerToken * quantityToBuy; - - // // Seller approves buyer for listing - // vm.prank(seller); - // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); - - // // Verify that seller is owner of listed tokens, pre-sale. - // uint256[] memory tokenIds = new uint256[](1); - // tokenIds[0] = 0; - // assertIsOwnerERC721(address(erc721), seller, tokenIds); - // assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); - - // // Mint requisite total price to buyer. - // erc20.mint(buyer, totalPrice); - // assertBalERC20Eq(address(erc20), buyer, totalPrice); - // assertBalERC20Eq(address(erc20), seller, 0); - - // // Don't approve marketplace to transfer currency - // vm.prank(buyer); - // erc20.increaseAllowance(marketplace, totalPrice); - - // // Buy tokens from listing. - // vm.warp(listing.startTimestamp); - // vm.prank(buyer); - // vm.expectRevert("Buying invalid quantity"); - // MintraDirectListingsLogicStandalone(marketplace).buyFromListing( - // listingId, - // buyFor, - // quantityToBuy, - // currency, - // totalPrice - // ); - // } + function test_revert_buyFromListing_invalidCurrency() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + + assertEq(listing.currency, address(erc20)); + assertEq( + MintraDirectListingsLogicStandalone(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + false + ); + + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Paying in invalid currency."); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = NATIVE_TOKEN; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price + assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.approve(marketplace, 0); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_buyingZeroQuantity() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = 0; // Buying zero quantity + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } /*/////////////////////////////////////////////////////////////// View functions @@ -2021,43 +2101,60 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); } - // function test_audit_native_tokens_locked() public { - // (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); + function test_audit_native_tokens_locked() public { + (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingListing.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingListing.assetContract, address(erc721)); + + vm.warp(existingListing.startTimestamp); + + // No ether is locked in contract + assertEq(marketplace.balance, 0); - // uint256[] memory tokenIds = new uint256[](1); - // tokenIds[0] = existingListing.tokenId; + // buy from listing + erc20.mint(buyer, 10 ether); + vm.deal(buyer, 1 ether); - // // Verify existing auction at `auctionId` - // assertEq(existingListing.assetContract, address(erc721)); + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + + vm.startPrank(buyer); + erc20.approve(marketplace, 10 ether); - // vm.warp(existingListing.startTimestamp); + vm.expectRevert("Incorrect PLS amount sent"); - // // No ether is locked in contract - // assertEq(marketplace.balance, 0); + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyer; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = 1; - // // buy from listing - // erc20.mint(buyer, 10 ether); - // vm.deal(buyer, 1 ether); + address[] memory currencyArray = new address[](1); + currencyArray[0] = address(erc20); - // vm.prank(seller); - // MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingId, buyer, true); + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = 1 ether; - // vm.startPrank(buyer); - // erc20.approve(marketplace, 10 ether); + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing{ value: 1 ether }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); - // vm.expectRevert("Marketplace: invalid native tokens sent."); - // MintraDirectListingsLogicStandalone(marketplace).buyFromListing{ value: 1 ether }( - // listingId, - // buyer, - // 1, - // address(erc20), - // 1 ether - // ); - // vm.stopPrank(); + vm.stopPrank(); - // // 1 ether is temporary locked in contract - // assertEq(marketplace.balance, 0 ether); - // } + // 1 ether is temporary locked in contract + assertEq(marketplace.balance, 0 ether); + } function test_set_platform_fee() public { uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); From b62b5ed087d61641f1f07daf2373d257abeda4f5 Mon Sep 17 00:00:00 2001 From: hexlive Date: Tue, 2 Jan 2024 16:30:34 -0500 Subject: [PATCH 14/16] added some more tests for the bulk function --- .../MintraDirectListingStandalone.t.sol | 272 ++++++++++++++---- 1 file changed, 216 insertions(+), 56 deletions(-) diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index dd1eee0e1..f248e5f4d 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -120,7 +120,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_state_approvedCurrencies() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParams) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParams) = _setup_updateListing(0); address currencyToApprove = address(erc20); // same currency as main listing uint256 pricePerTokenForCurrency = 2 ether; @@ -380,10 +380,9 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Create listing //////////////////////////////////////////////////////////////*/ - function test_state_createListing_1155() public { + function createListing_1155(uint256 tokenId, uint256 totalListings) private returns (uint256 listingId) { // Sample listing parameters. address assetContract = address(erc1155); - uint256 tokenId = 0; uint256 quantity = 2; address currency = address(erc20); uint256 pricePerToken = 1 ether; @@ -420,7 +419,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { ); vm.prank(seller); - uint256 listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); + listingId = MintraDirectListingsLogicStandalone(marketplace).createListing(listingParams); // Test consequent state of the contract. @@ -428,7 +427,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); // Total listings incremented - assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), 1); + assertEq(MintraDirectListingsLogicStandalone(marketplace).totalListings(), totalListings); // Fetch listing and verify state. IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); @@ -444,6 +443,8 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assertEq(listing.endTimestamp, endTimestamp); assertEq(listing.reserved, reserved); assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC1155)); + + return listingId; } function test_state_createListing() public { @@ -846,13 +847,11 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Update listing //////////////////////////////////////////////////////////////*/ - function _setup_updateListing() - private - returns (uint256 listingId, IDirectListings.ListingParameters memory listingParams) - { - // Sample listing parameters. + function _setup_updateListing( + uint256 tokenId + ) private returns (uint256 listingId, IDirectListings.ListingParameters memory listingParams) { + // listing parameters. address assetContract = address(erc721); - uint256 tokenId = 0; uint256 quantity = 1; address currency = address(erc20); uint256 pricePerToken = 1 ether; @@ -888,7 +887,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_state_updateListing() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -928,7 +927,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_state_updateListing_start_time_in_past() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -976,7 +975,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_notListingCreator() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -993,7 +992,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_notOwnerOfListedToken() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens but NOT to seller. A new tokenId will be listed. address notSeller = getActor(1000); @@ -1013,7 +1012,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_notApprovedMarketplaceToTransferToken() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -1033,7 +1032,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_listingZeroQuantity() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -1051,7 +1050,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_listingInvalidQuantity() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -1069,7 +1068,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_listingNonERC721OrERC1155Token() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -1087,7 +1086,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_invalidStartTimestamp() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -1107,7 +1106,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_updateListing_invalidEndTimestamp() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. _setupERC721BalanceForSeller(seller, 1); @@ -1129,13 +1128,15 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Cancel listing //////////////////////////////////////////////////////////////*/ - function _setup_cancelListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { - (listingId, ) = _setup_updateListing(); + function _setup_cancelListing( + uint256 tokenId + ) private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(tokenId); listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); } function test_state_cancelListing() public { - (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(); + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(0); // Verify existing listing at `listingId` assertEq(existingListingAtId.assetContract, address(erc721)); @@ -1151,7 +1152,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_cancelListing_notListingCreator() public { - (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(); + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(0); // Verify existing listing at `listingId` assertEq(existingListingAtId.assetContract, address(erc721)); @@ -1163,7 +1164,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_cancelListing_nonExistentListing() public { - _setup_cancelListing(); + _setup_cancelListing(0); // Verify no listing exists at `nexListingId` uint256 nextListingId = MintraDirectListingsLogicStandalone(marketplace).totalListings(); @@ -1177,12 +1178,12 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Approve buyer for listing //////////////////////////////////////////////////////////////*/ - function _setup_approveBuyerForListing() private returns (uint256 listingId) { - (listingId, ) = _setup_updateListing(); + function _setup_approveBuyerForListing(uint256 tokenId) private returns (uint256 listingId) { + (listingId, ) = _setup_updateListing(tokenId); } function test_state_approveBuyerForListing() public { - uint256 listingId = _setup_approveBuyerForListing(); + uint256 listingId = _setup_approveBuyerForListing(0); bool toApprove = true; assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, true); @@ -1195,7 +1196,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_approveBuyerForListing_notListingCreator() public { - uint256 listingId = _setup_approveBuyerForListing(); + uint256 listingId = _setup_approveBuyerForListing(0); bool toApprove = true; assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, true); @@ -1208,7 +1209,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_approveBuyerForListing_listingNotReserved() public { - (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(); + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); bool toApprove = true; assertEq(MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).reserved, true); @@ -1230,12 +1231,12 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Approve currency for listing //////////////////////////////////////////////////////////////*/ - function _setup_approveCurrencyForListing() private returns (uint256 listingId) { - (listingId, ) = _setup_updateListing(); + function _setup_approveCurrencyForListing(uint256 tokenId) private returns (uint256 listingId) { + (listingId, ) = _setup_updateListing(tokenId); } function test_state_approveCurrencyForListing() public { - uint256 listingId = _setup_approveCurrencyForListing(); + uint256 listingId = _setup_approveCurrencyForListing(0); address currencyToApprove = NATIVE_TOKEN; uint256 pricePerTokenForCurrency = 2 ether; @@ -1258,7 +1259,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_approveCurrencyForListing_notListingCreator() public { - uint256 listingId = _setup_approveCurrencyForListing(); + uint256 listingId = _setup_approveCurrencyForListing(0); address currencyToApprove = NATIVE_TOKEN; uint256 pricePerTokenForCurrency = 2 ether; @@ -1274,7 +1275,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_approveCurrencyForListing_reApprovingMainCurrency() public { - uint256 listingId = _setup_approveCurrencyForListing(); + uint256 listingId = _setup_approveCurrencyForListing(0); address currencyToApprove = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId).currency; uint256 pricePerTokenForCurrency = 2 ether; @@ -1292,8 +1293,10 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { Buy from listing //////////////////////////////////////////////////////////////*/ - function _setup_buyFromListing() private returns (uint256 listingId, IDirectListings.Listing memory listing) { - (listingId, ) = _setup_updateListing(); + function _setup_buyFromListing( + uint256 tokenId + ) private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(tokenId); listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); } @@ -1370,7 +1373,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_state_buyFromListing_721() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1443,12 +1446,87 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } + function test_state_buyFromListing_multi_721() public { + vm.prank(seller); + (uint256 listingIdOne, IDirectListings.Listing memory listingOne) = _setup_buyFromListing(0); + vm.prank(seller); + (uint256 listingIdTwo, IDirectListings.Listing memory listingTwo) = _setup_buyFromListing(1); + + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingIdOne, buyer, true); + vm.prank(seller); + MintraDirectListingsLogicStandalone(marketplace).approveBuyerForListing(listingIdTwo, buyer, true); + + address buyFor = buyer; + uint256 quantityToBuy = listingOne.quantity; + address currency = listingOne.currency; + uint256 pricePerToken = listingOne.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice + totalPrice); + + // Buy tokens from listing. + vm.warp(listingTwo.startTimestamp); + { + uint256[] memory listingIdArray = new uint256[](2); + listingIdArray[0] = listingIdOne; + listingIdArray[1] = listingIdTwo; + + address[] memory buyForArray = new address[](2); + buyForArray[0] = buyFor; + buyForArray[1] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](2); + quantityToBuyArray[0] = quantityToBuy; + quantityToBuyArray[1] = quantityToBuy; + + address[] memory currencyArray = new address[](2); + currencyArray[0] = currency; + currencyArray[1] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](2); + expectedTotalPriceArray[0] = totalPrice; + expectedTotalPriceArray[1] = totalPrice; + + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + uint256 sellerPayout = totalPrice + totalPrice - platformFee - platformFee; + assertBalERC20Eq(address(erc20), seller, sellerPayout); + } + function test_state_buyFromListing_1155() public { // Create the listing - test_state_createListing_1155(); - - //(uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); - uint256 listingId = 0; + uint256 listingId = createListing_1155(0, 1); IDirectListings.Listing memory listing = MintraDirectListingsLogicStandalone(marketplace).getListing(listingId); @@ -1523,8 +1601,91 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } } + function test_state_buyFromListing_multi_1155() public { + vm.prank(seller); + uint256 listingIdOne = createListing_1155(0, 1); + IDirectListings.Listing memory listingOne = MintraDirectListingsLogicStandalone(marketplace).getListing( + listingIdOne + ); + + vm.prank(seller); + uint256 listingIdTwo = createListing_1155(1, 2); + IDirectListings.Listing memory listingTwo = MintraDirectListingsLogicStandalone(marketplace).getListing( + listingIdTwo + ); + + address buyFor = buyer; + uint256 quantityToBuy = listingOne.quantity; + address currency = listingOne.currency; + uint256 pricePerToken = listingOne.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 2; + amounts[1] = 2; + + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice + totalPrice); + + // Buy tokens from listing. + vm.warp(listingTwo.startTimestamp); + { + uint256[] memory listingIdArray = new uint256[](2); + listingIdArray[0] = listingIdOne; + listingIdArray[1] = listingIdTwo; + + address[] memory buyForArray = new address[](2); + buyForArray[0] = buyFor; + buyForArray[1] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](2); + quantityToBuyArray[0] = quantityToBuy; + quantityToBuyArray[1] = quantityToBuy; + + address[] memory currencyArray = new address[](2); + currencyArray[0] = currency; + currencyArray[1] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](2); + expectedTotalPriceArray[0] = totalPrice; + expectedTotalPriceArray[1] = totalPrice; + + vm.prank(buyer); + MintraDirectListingsLogicStandalone(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertBalERC1155Eq(address(erc1155), buyer, tokenIds, amounts); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + uint256 sellerPayout = totalPrice + totalPrice - platformFee - platformFee; + assertBalERC20Eq(address(erc20), seller, sellerPayout); + } + function test_state_bulkBuyFromListing_nativeToken() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1598,7 +1759,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_bulkBuyFromListing_nativeToken_incorrectValueSent() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1654,7 +1815,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_buyFromListing_unexpectedTotalPrice() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1711,7 +1872,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_buyFromListing_invalidCurrency() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1774,7 +1935,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1831,7 +1992,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity; @@ -1888,7 +2049,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_buyFromListing_buyingZeroQuantity() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = 0; // Buying zero quantity @@ -1945,7 +2106,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { - (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); address buyFor = buyer; uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. @@ -2006,7 +2167,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { //////////////////////////////////////////////////////////////*/ function test_getAllListing() public { // Create the listing - test_state_createListing_1155(); + createListing_1155(0, 1); IDirectListings.Listing[] memory listings = MintraDirectListingsLogicStandalone(marketplace).getAllListings( 0, @@ -2029,7 +2190,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { function test_getAllValidListings() public { // Create the listing - test_state_createListing_1155(); + createListing_1155(0, 1); IDirectListings.Listing[] memory listingsAll = MintraDirectListingsLogicStandalone(marketplace).getAllListings( 0, @@ -2058,7 +2219,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { function test_currencyPriceForListing_fail() public { // Create the listing - test_state_createListing_1155(); + createListing_1155(0, 1); vm.expectRevert("Currency not approved for listing"); MintraDirectListingsLogicStandalone(marketplace).currencyPriceForListing(0, address(erc20Aux)); @@ -2102,7 +2263,7 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { } function test_audit_native_tokens_locked() public { - (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(); + (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(0); uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = existingListing.tokenId; @@ -2165,7 +2326,6 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { platformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); - console.log("platformFeeBps", platformFeeBps); assertEq(platformFeeBps, 369); } From 96f014facd5e236806611bfe45ace9cb9baa7038 Mon Sep 17 00:00:00 2001 From: hexlive Date: Tue, 2 Jan 2024 16:41:40 -0500 Subject: [PATCH 15/16] Add a simple fuzz test --- .../marketplace/MintraDirectListingStandalone.t.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/marketplace/MintraDirectListingStandalone.t.sol b/src/test/marketplace/MintraDirectListingStandalone.t.sol index f248e5f4d..55c3c4f7d 100644 --- a/src/test/marketplace/MintraDirectListingStandalone.t.sol +++ b/src/test/marketplace/MintraDirectListingStandalone.t.sol @@ -2329,6 +2329,17 @@ contract MintraDirectListingsLogicStandaloneTest is BaseTest, IExtension { assertEq(platformFeeBps, 369); } + function test_fuzz_set_platform_fee(uint256 platformFeeBps) public { + vm.assume(platformFeeBps <= 369); + + vm.prank(wizard); + MintraDirectListingsLogicStandalone(marketplace).setPlatformFeeBps(platformFeeBps); + + uint256 expectedPlatformFeeBps = MintraDirectListingsLogicStandalone(marketplace).platformFeeBps(); + + assertEq(expectedPlatformFeeBps, platformFeeBps); + } + function test_set_platform_fee_fail() public { vm.prank(wizard); vm.expectRevert("Fee not in range"); From 226d8dd829aa4336b967f35a29ba64400fd10e74 Mon Sep 17 00:00:00 2001 From: hexlive Date: Fri, 5 Jan 2024 16:02:32 -0500 Subject: [PATCH 16/16] Q1 - Rename contract to a more accurate name --- .../direct-listings/MintraDirectListings.sol | 683 +++++ .../marketplace/MintraDirectListings.t.sol | 2348 +++++++++++++++++ 2 files changed, 3031 insertions(+) create mode 100644 contracts/prebuilts/marketplace/direct-listings/MintraDirectListings.sol create mode 100644 src/test/marketplace/MintraDirectListings.t.sol diff --git a/contracts/prebuilts/marketplace/direct-listings/MintraDirectListings.sol b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListings.sol new file mode 100644 index 000000000..1683b503a --- /dev/null +++ b/contracts/prebuilts/marketplace/direct-listings/MintraDirectListings.sol @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb.com / mintra.ai + +import "./DirectListingsStorage.sol"; + +// ====== External imports ====== +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +// ====== Internal imports ====== +import "../../../eip/interface/IERC721.sol"; +import "../../../extension/Multicall.sol"; +import "../../../extension/upgradeable/ReentrancyGuard.sol"; +import { CurrencyTransferLib } from "../../../lib/CurrencyTransferLib.sol"; + +/** + * @author thirdweb.com / mintra.ai + */ +contract MintraDirectListings is IDirectListings, Multicall, ReentrancyGuard { + /*/////////////////////////////////////////////////////////////// + Mintra + //////////////////////////////////////////////////////////////*/ + struct Royalty { + address receiver; + uint256 basisPoints; + } + + event MintraNewSale( + uint256 listingId, + address buyer, + uint256 quantityBought, + uint256 totalPricePaid, + address currency + ); + + event MintraRoyaltyTransfered( + address assetContract, + uint256 tokenId, + uint256 listingId, + uint256 totalPrice, + uint256 royaltyAmount, + uint256 platformFee, + address royaltyRecipient, + address currency + ); + + event RoyaltyUpdated(address assetContract, uint256 royaltyAmount, address royaltyRecipient); + + address public immutable wizard; + address private immutable mintTokenAddress; + address public immutable platformFeeRecipient; + uint256 public platformFeeBps = 225; + uint256 public platformFeeBpsMint = 150; + mapping(address => Royalty) public royalties; + + /*/////////////////////////////////////////////////////////////// + Constants / Immutables + //////////////////////////////////////////////////////////////*/ + + /// @dev The max bps of the contract. So, 10_000 == 100 % + uint64 private constant MAX_BPS = 10_000; + + /// @dev The address of the native token wrapper contract. + address private immutable nativeTokenWrapper; + + /*/////////////////////////////////////////////////////////////// + Modifier + //////////////////////////////////////////////////////////////*/ + + modifier onlyWizard() { + require(msg.sender == wizard, "Not Wizard"); + _; + } + + /// @dev Checks whether caller is a listing creator. + modifier onlyListingCreator(uint256 _listingId) { + require( + _directListingsStorage().listings[_listingId].listingCreator == msg.sender, + "Marketplace: not listing creator." + ); + _; + } + + /// @dev Checks whether a listing exists. + modifier onlyExistingListing(uint256 _listingId) { + require( + _directListingsStorage().listings[_listingId].status == IDirectListings.Status.CREATED, + "Marketplace: invalid listing." + ); + _; + } + + /*/////////////////////////////////////////////////////////////// + Constructor logic + //////////////////////////////////////////////////////////////*/ + + constructor( + address _nativeTokenWrapper, + address _mintTokenAddress, + address _platformFeeRecipient, + address _wizard + ) { + nativeTokenWrapper = _nativeTokenWrapper; + mintTokenAddress = _mintTokenAddress; + platformFeeRecipient = _platformFeeRecipient; + wizard = _wizard; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice List NFTs (ERC721 or ERC1155) for sale at a fixed price. + function createListing(ListingParameters calldata _params) external returns (uint256 listingId) { + listingId = _getNextListingId(); + address listingCreator = msg.sender; + TokenType tokenType = _getTokenType(_params.assetContract); + + uint128 startTime = _params.startTimestamp; + uint128 endTime = _params.endTimestamp; + require(startTime < endTime, "Marketplace: endTimestamp not greater than startTimestamp."); + if (startTime < block.timestamp) { + require(startTime + 60 minutes >= block.timestamp, "Marketplace: invalid startTimestamp."); + + startTime = uint128(block.timestamp); + endTime = endTime == type(uint128).max + ? endTime + : startTime + (_params.endTimestamp - _params.startTimestamp); + } + + _validateNewListing(_params, tokenType); + + Listing memory listing = Listing({ + listingId: listingId, + listingCreator: listingCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + pricePerToken: _params.pricePerToken, + startTimestamp: startTime, + endTimestamp: endTime, + reserved: _params.reserved, + tokenType: tokenType, + status: IDirectListings.Status.CREATED + }); + + _directListingsStorage().listings[listingId] = listing; + + emit NewListing(listingCreator, listingId, _params.assetContract, listing); + + return listingId; + } + + /// @notice Update parameters of a listing of NFTs. + function updateListing( + uint256 _listingId, + ListingParameters memory _params + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + address listingCreator = msg.sender; + Listing memory listing = _directListingsStorage().listings[_listingId]; + TokenType tokenType = _getTokenType(_params.assetContract); + + require(listing.endTimestamp > block.timestamp, "Marketplace: listing expired."); + + require( + listing.assetContract == _params.assetContract && listing.tokenId == _params.tokenId, + "Marketplace: cannot update what token is listed." + ); + + uint128 startTime = _params.startTimestamp; + uint128 endTime = _params.endTimestamp; + require(startTime < endTime, "Marketplace: endTimestamp not greater than startTimestamp."); + require( + listing.startTimestamp > block.timestamp || + (startTime == listing.startTimestamp && endTime > block.timestamp), + "Marketplace: listing already active." + ); + if (startTime != listing.startTimestamp && startTime < block.timestamp) { + require(startTime + 60 minutes >= block.timestamp, "Marketplace: invalid startTimestamp."); + + startTime = uint128(block.timestamp); + + endTime = endTime == listing.endTimestamp || endTime == type(uint128).max + ? endTime + : startTime + (_params.endTimestamp - _params.startTimestamp); + } + + { + uint256 _approvedCurrencyPrice = _directListingsStorage().currencyPriceForListing[_listingId][ + _params.currency + ]; + require( + _approvedCurrencyPrice == 0 || _params.pricePerToken == _approvedCurrencyPrice, + "Marketplace: price different from approved price" + ); + } + + _validateNewListing(_params, tokenType); + + listing = Listing({ + listingId: _listingId, + listingCreator: listingCreator, + assetContract: _params.assetContract, + tokenId: _params.tokenId, + quantity: _params.quantity, + currency: _params.currency, + pricePerToken: _params.pricePerToken, + startTimestamp: startTime, + endTimestamp: endTime, + reserved: _params.reserved, + tokenType: tokenType, + status: IDirectListings.Status.CREATED + }); + + _directListingsStorage().listings[_listingId] = listing; + + emit UpdatedListing(listingCreator, _listingId, _params.assetContract, listing); + } + + /// @notice Cancel a listing. + function cancelListing(uint256 _listingId) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + _directListingsStorage().listings[_listingId].status = IDirectListings.Status.CANCELLED; + emit CancelledListing(msg.sender, _listingId); + } + + /// @notice Approve a buyer to buy from a reserved listing. + function approveBuyerForListing( + uint256 _listingId, + address _buyer, + bool _toApprove + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + require(_directListingsStorage().listings[_listingId].reserved, "Marketplace: listing not reserved."); + + _directListingsStorage().isBuyerApprovedForListing[_listingId][_buyer] = _toApprove; + + emit BuyerApprovedForListing(_listingId, _buyer, _toApprove); + } + + /// @notice Approve a currency as a form of payment for the listing. + function approveCurrencyForListing( + uint256 _listingId, + address _currency, + uint256 _pricePerTokenInCurrency + ) external onlyExistingListing(_listingId) onlyListingCreator(_listingId) { + Listing memory listing = _directListingsStorage().listings[_listingId]; + require( + _currency != listing.currency || _pricePerTokenInCurrency == listing.pricePerToken, + "Marketplace: approving listing currency with different price." + ); + require( + _directListingsStorage().currencyPriceForListing[_listingId][_currency] != _pricePerTokenInCurrency, + "Marketplace: price unchanged." + ); + + _directListingsStorage().currencyPriceForListing[_listingId][_currency] = _pricePerTokenInCurrency; + + emit CurrencyApprovedForListing(_listingId, _currency, _pricePerTokenInCurrency); + } + + function bulkBuyFromListing( + uint256[] memory _listingId, + address[] memory _buyFor, + uint256[] memory _quantity, + address[] memory _currency, + uint256[] memory _expectedTotalPrice + ) external payable nonReentrant { + uint256 totalAmountPls = 0; + // Iterate over each tokenId + for (uint256 i = 0; i < _listingId.length; i++) { + // Are we buying this item in PLS + uint256 price; + + Listing memory listing = _directListingsStorage().listings[_listingId[i]]; + + require(listing.status == IDirectListings.Status.CREATED, "Marketplace: invalid listing."); + + if (_currency[i] == CurrencyTransferLib.NATIVE_TOKEN) { + //calculate total amount for items being sold for PLS + if (_directListingsStorage().currencyPriceForListing[_listingId[i]][_currency[i]] > 0) { + price = + _quantity[i] * + _directListingsStorage().currencyPriceForListing[_listingId[i]][_currency[i]]; + } else { + require(_currency[i] == listing.currency, "Paying in invalid currency."); + price = _quantity[i] * listing.pricePerToken; + } + + totalAmountPls += price; + } + + // Call the buy function for the current tokenId + _buyFromListing(listing, _buyFor[i], _quantity[i], _currency[i], _expectedTotalPrice[i]); + } + + // Make sure that the total price for items bought with PLS is equal to the amount sent + require(msg.value == totalAmountPls || (totalAmountPls == 0 && msg.value == 0), "Incorrect PLS amount sent"); + } + + /// @notice Buy NFTs from a listing. + function _buyFromListing( + Listing memory listing, + address _buyFor, + uint256 _quantity, + address _currency, + uint256 _expectedTotalPrice + ) internal { + uint256 listingId = listing.listingId; + address buyer = msg.sender; + + require( + !listing.reserved || _directListingsStorage().isBuyerApprovedForListing[listingId][buyer], + "buyer not approved" + ); + require(_quantity > 0 && _quantity <= listing.quantity, "Buying invalid quantity"); + require( + block.timestamp < listing.endTimestamp && block.timestamp >= listing.startTimestamp, + "not within sale window." + ); + + require( + _validateOwnershipAndApproval( + listing.listingCreator, + listing.assetContract, + listing.tokenId, + _quantity, + listing.tokenType + ), + "Marketplace: not owner or approved tokens." + ); + + uint256 targetTotalPrice; + + // Check: is the buyer paying in a currency that the listing creator approved + if (_directListingsStorage().currencyPriceForListing[listingId][_currency] > 0) { + targetTotalPrice = _quantity * _directListingsStorage().currencyPriceForListing[listingId][_currency]; + } else { + require(_currency == listing.currency, "Paying in invalid currency."); + targetTotalPrice = _quantity * listing.pricePerToken; + } + + // Check: is the buyer paying the price that the buyer is expecting to pay. + // This is to prevent attack where the seller could change the price + // right before the buyers tranaction executes. + require(targetTotalPrice == _expectedTotalPrice, "Unexpected total price"); + + if (_currency != CurrencyTransferLib.NATIVE_TOKEN) { + _validateERC20BalAndAllowance(buyer, _currency, targetTotalPrice); + } + + if (listing.quantity == _quantity) { + _directListingsStorage().listings[listingId].status = IDirectListings.Status.COMPLETED; + } + _directListingsStorage().listings[listingId].quantity -= _quantity; + + _payout(buyer, listing.listingCreator, _currency, targetTotalPrice, listing); + + _transferListingTokens(listing.listingCreator, _buyFor, _quantity, listing); + + emit MintraNewSale(listing.listingId, buyer, _quantity, targetTotalPrice, _currency); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the total number of listings created. + * @dev At any point, the return value is the ID of the next listing created. + */ + function totalListings() external view returns (uint256) { + return _directListingsStorage().totalListings; + } + + /// @notice Returns whether a buyer is approved for a listing. + function isBuyerApprovedForListing(uint256 _listingId, address _buyer) external view returns (bool) { + return _directListingsStorage().isBuyerApprovedForListing[_listingId][_buyer]; + } + + /// @notice Returns whether a currency is approved for a listing. + function isCurrencyApprovedForListing(uint256 _listingId, address _currency) external view returns (bool) { + return _directListingsStorage().currencyPriceForListing[_listingId][_currency] > 0; + } + + /// @notice Returns the price per token for a listing, in the given currency. + function currencyPriceForListing(uint256 _listingId, address _currency) external view returns (uint256) { + if (_directListingsStorage().currencyPriceForListing[_listingId][_currency] == 0) { + revert("Currency not approved for listing"); + } + + return _directListingsStorage().currencyPriceForListing[_listingId][_currency]; + } + + /// @notice Returns all non-cancelled listings. + function getAllListings(uint256 _startId, uint256 _endId) external view returns (Listing[] memory _allListings) { + require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); + + _allListings = new Listing[](_endId - _startId + 1); + + for (uint256 i = _startId; i <= _endId; i += 1) { + _allListings[i - _startId] = _directListingsStorage().listings[i]; + } + } + + /** + * @notice Returns all valid listings between the start and end Id (both inclusive) provided. + * A valid listing is where the listing creator still owns and has approved Marketplace + * to transfer the listed NFTs. + */ + function getAllValidListings( + uint256 _startId, + uint256 _endId + ) external view returns (Listing[] memory _validListings) { + require(_startId <= _endId && _endId < _directListingsStorage().totalListings, "invalid range"); + + Listing[] memory _listings = new Listing[](_endId - _startId + 1); + uint256 _listingCount; + + for (uint256 i = _startId; i <= _endId; i += 1) { + _listings[i - _startId] = _directListingsStorage().listings[i]; + if (_validateExistingListing(_listings[i - _startId])) { + _listingCount += 1; + } + } + + _validListings = new Listing[](_listingCount); + uint256 index = 0; + uint256 count = _listings.length; + for (uint256 i = 0; i < count; i += 1) { + if (_validateExistingListing(_listings[i])) { + _validListings[index++] = _listings[i]; + } + } + } + + /// @notice Returns a listing at a particular listing ID. + function getListing(uint256 _listingId) external view returns (Listing memory listing) { + listing = _directListingsStorage().listings[_listingId]; + } + + /** + * @notice Set or update the royalty for a collection + * @dev Sets or updates the royalty for a collection to a new value + * @param _collectionAddress Address of the collection to set the royalty for + * @param _royaltyInBasisPoints New royalty value, in basis points (1 basis point = 0.01%) + */ + function createOrUpdateRoyalty( + address _collectionAddress, + uint256 _royaltyInBasisPoints, + address receiver + ) public nonReentrant { + require(_collectionAddress != address(0), "_collectionAddress is not set"); + require(_royaltyInBasisPoints >= 0 && _royaltyInBasisPoints <= 10000, "Royalty not in range"); + require(receiver != address(0), "receiver is not set"); + + // Check that the caller is the owner/creator of the collection contract + require(Ownable(_collectionAddress).owner() == msg.sender, "Unauthorized"); + + // Create a new Royalty object with the given value and store it in the royalties mapping + Royalty memory royalty = Royalty(receiver, _royaltyInBasisPoints); + royalties[_collectionAddress] = royalty; + + // Emit a RoyaltyUpdated + emit RoyaltyUpdated(_collectionAddress, _royaltyInBasisPoints, receiver); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the next listing Id. + function _getNextListingId() internal returns (uint256 id) { + id = _directListingsStorage().totalListings; + _directListingsStorage().totalListings += 1; + } + + /// @dev Returns the interface supported by a contract. + function _getTokenType(address _assetContract) internal view returns (TokenType tokenType) { + if (IERC165(_assetContract).supportsInterface(type(IERC1155).interfaceId)) { + tokenType = TokenType.ERC1155; + } else if (IERC165(_assetContract).supportsInterface(type(IERC721).interfaceId)) { + tokenType = TokenType.ERC721; + } else { + revert("Marketplace: listed token must be ERC1155 or ERC721."); + } + } + + /// @dev Checks whether the listing creator owns and has approved marketplace to transfer listed tokens. + function _validateNewListing(ListingParameters memory _params, TokenType _tokenType) internal view { + require(_params.quantity > 0, "Marketplace: listing zero quantity."); + require(_params.quantity == 1 || _tokenType == TokenType.ERC1155, "Marketplace: listing invalid quantity."); + + require( + _validateOwnershipAndApproval( + msg.sender, + _params.assetContract, + _params.tokenId, + _params.quantity, + _tokenType + ), + "Marketplace: not owner or approved tokens." + ); + } + + /// @dev Checks whether the listing exists, is active, and if the lister has sufficient balance. + function _validateExistingListing(Listing memory _targetListing) internal view returns (bool isValid) { + isValid = + _targetListing.startTimestamp <= block.timestamp && + _targetListing.endTimestamp > block.timestamp && + _targetListing.status == IDirectListings.Status.CREATED && + _validateOwnershipAndApproval( + _targetListing.listingCreator, + _targetListing.assetContract, + _targetListing.tokenId, + _targetListing.quantity, + _targetListing.tokenType + ); + } + + /// @dev Validates that `_tokenOwner` owns and has approved Marketplace to transfer NFTs. + function _validateOwnershipAndApproval( + address _tokenOwner, + address _assetContract, + uint256 _tokenId, + uint256 _quantity, + TokenType _tokenType + ) internal view returns (bool isValid) { + address market = address(this); + + if (_tokenType == TokenType.ERC1155) { + isValid = + IERC1155(_assetContract).balanceOf(_tokenOwner, _tokenId) >= _quantity && + IERC1155(_assetContract).isApprovedForAll(_tokenOwner, market); + } else if (_tokenType == TokenType.ERC721) { + address owner; + address operator; + + // failsafe for reverts in case of non-existent tokens + try IERC721(_assetContract).ownerOf(_tokenId) returns (address _owner) { + owner = _owner; + + // Nesting the approval check inside this try block, to run only if owner check doesn't revert. + // If the previous check for owner fails, then the return value will always evaluate to false. + try IERC721(_assetContract).getApproved(_tokenId) returns (address _operator) { + operator = _operator; + } catch {} + } catch {} + + isValid = + owner == _tokenOwner && + (operator == market || IERC721(_assetContract).isApprovedForAll(_tokenOwner, market)); + } + } + + /// @dev Validates that `_tokenOwner` owns and has approved Markeplace to transfer the appropriate amount of currency + function _validateERC20BalAndAllowance(address _tokenOwner, address _currency, uint256 _amount) internal view { + require( + IERC20(_currency).balanceOf(_tokenOwner) >= _amount && + IERC20(_currency).allowance(_tokenOwner, address(this)) >= _amount, + "!BAL20" + ); + } + + /// @dev Transfers tokens listed for sale in a direct or auction listing. + function _transferListingTokens(address _from, address _to, uint256 _quantity, Listing memory _listing) internal { + if (_listing.tokenType == TokenType.ERC1155) { + IERC1155(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, _quantity, ""); + } else if (_listing.tokenType == TokenType.ERC721) { + IERC721(_listing.assetContract).safeTransferFrom(_from, _to, _listing.tokenId, ""); + } + } + + /// @dev Pays out stakeholders in a sale. + function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Listing memory _listing + ) internal { + uint256 amountRemaining; + uint256 platformFeeCut; + + // Payout platform fee + { + // Descrease platform fee for mint token + if (_currencyToUse == mintTokenAddress) { + platformFeeCut = (_totalPayoutAmount * platformFeeBpsMint) / MAX_BPS; + } else { + platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS; + } + + // Transfer platform fee + CurrencyTransferLib.transferCurrency(_currencyToUse, _payer, platformFeeRecipient, platformFeeCut); + + amountRemaining = _totalPayoutAmount - platformFeeCut; + } + + // Payout royalties + { + // Get royalty recipients and amounts + (address royaltyRecipient, uint256 royaltyAmount) = processRoyalty( + _listing.assetContract, + _listing.tokenId, + _totalPayoutAmount + ); + + if (royaltyAmount > 0) { + // Check payout amount remaining is enough to cover royalty payment + require(amountRemaining >= royaltyAmount, "fees exceed the price"); + + // Transfer royalty + CurrencyTransferLib.transferCurrency(_currencyToUse, _payer, royaltyRecipient, royaltyAmount); + + amountRemaining = amountRemaining - royaltyAmount; + + emit MintraRoyaltyTransfered( + _listing.assetContract, + _listing.tokenId, + _listing.listingId, + _totalPayoutAmount, + royaltyAmount, + platformFeeCut, + royaltyRecipient, + _currencyToUse + ); + } + } + + // Distribute price to token owner + CurrencyTransferLib.transferCurrency(_currencyToUse, _payer, _payee, amountRemaining); + } + + function processRoyalty( + address _tokenAddress, + uint256 _tokenId, + uint256 _price + ) internal view returns (address royaltyReceiver, uint256 royaltyAmount) { + // Check if collection has royalty using ERC2981 + if (isERC2981(_tokenAddress)) { + (royaltyReceiver, royaltyAmount) = IERC2981(_tokenAddress).royaltyInfo(_tokenId, _price); + } else { + royaltyAmount = (_price * royalties[_tokenAddress].basisPoints) / 10000; + royaltyReceiver = royalties[_tokenAddress].receiver; + } + + return (royaltyReceiver, royaltyAmount); + } + + /** + * @notice This function checks if a given contract is ERC2981 compliant + * @dev This function is called internally and cannot be accessed outside the contract + * @param _contract The address of the contract to check + * @return A boolean indicating whether the contract is ERC2981 compliant or not + */ + function isERC2981(address _contract) internal view returns (bool) { + try IERC2981(_contract).royaltyInfo(0, 0) returns (address, uint256) { + return true; + } catch { + return false; + } + } + + /// @dev Returns the DirectListings storage. + function _directListingsStorage() internal pure returns (DirectListingsStorage.Data storage data) { + data = DirectListingsStorage.data(); + } + + /** + * @notice Update the market fee percentage + * @dev Updates the market fee percentage to a new value + * @param _platformFeeBps New value for the market fee percentage + */ + function setPlatformFeeBps(uint256 _platformFeeBps) public onlyWizard { + require(_platformFeeBps <= 369, "Fee not in range"); + + platformFeeBps = _platformFeeBps; + } +} diff --git a/src/test/marketplace/MintraDirectListings.t.sol b/src/test/marketplace/MintraDirectListings.t.sol new file mode 100644 index 000000000..e123ed446 --- /dev/null +++ b/src/test/marketplace/MintraDirectListings.t.sol @@ -0,0 +1,2348 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../mocks/MockRoyaltyEngineV1.sol"; + +import { IDirectListings } from "contracts/prebuilts/marketplace/IMarketplace.sol"; +import { MintraDirectListings } from "contracts/prebuilts/marketplace/direct-listings/MintraDirectListings.sol"; +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +import { MockERC721Ownable } from "../mocks/MockERC721Ownable.sol"; + +contract MintraDirectListingsTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + address public wizard; + address public collectionOwner; + + MintraDirectListings public mintraDirectListingsLogicStandalone; + MockERC721Ownable public erc721Ownable; + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + wizard = getActor(4); + collectionOwner = getActor(5); + + // Deploy implementation. + mintraDirectListingsLogicStandalone = new MintraDirectListings( + address(weth), + address(erc20Aux), + address(platformFeeRecipient), + address(wizard) + ); + marketplace = address(mintraDirectListingsLogicStandalone); + + vm.prank(collectionOwner); + erc721Ownable = new MockERC721Ownable(); + + //vm.prank(marketplaceDeployer); + + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + } + + function _setupERC721BalanceForSeller(address _seller, uint256 _numOfTokens) private { + erc721.mint(_seller, _numOfTokens); + } + + function test_state_initial() public { + uint256 totalListings = MintraDirectListings(marketplace).totalListings(); + assertEq(totalListings, 0); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_getValidListings_burnListedTokens() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + MintraDirectListings(marketplace).createListing(listingParams); + + // Total listings incremented + assertEq(MintraDirectListings(marketplace).totalListings(), 1); + + // burn listed token + vm.prank(seller); + erc721.burn(0); + + vm.warp(150); + // Fetch listing and verify state. + uint256 totalListings = MintraDirectListings(marketplace).totalListings(); + assertEq(MintraDirectListings(marketplace).getAllValidListings(0, totalListings - 1).length, 0); + } + + function test_state_approvedCurrencies() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParams) = _setup_updateListing(0); + address currencyToApprove = address(erc20); // same currency as main listing + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves currency for listing. + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + MintraDirectListings(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + + // change currency + currencyToApprove = NATIVE_TOKEN; + + vm.prank(seller); + MintraDirectListings(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + + assertEq( + MintraDirectListings(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + true + ); + assertEq( + MintraDirectListings(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), + pricePerTokenForCurrency + ); + + // should revert when updating listing with an approved currency but different price + listingParams.currency = NATIVE_TOKEN; + vm.prank(seller); + vm.expectRevert("Marketplace: price different from approved price"); + MintraDirectListings(marketplace).updateListing(listingId, listingParams); + + // change listingParams.pricePerToken to approved price + listingParams.pricePerToken = pricePerTokenForCurrency; + vm.prank(seller); + MintraDirectListings(marketplace).updateListing(listingId, listingParams); + } + + /*/////////////////////////////////////////////////////////////// + Royalty Tests (incl Royalty Engine / Registry) + //////////////////////////////////////////////////////////////*/ + + function _setupListingForRoyaltyTests(address erc721TokenAddress) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = erc721TokenAddress; + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 100 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Approve Marketplace to transfer token. + vm.prank(seller); + IERC721(erc721TokenAddress).setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + listingId = MintraDirectListings(marketplace).createListing(listingParams); + } + + function _buyFromListingForRoyaltyTests(uint256 listingId) private returns (uint256 totalPrice) { + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + totalPrice = pricePerToken * quantityToBuy; + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_noRoyaltyEngine_defaultERC2981Token() public { + // create token with ERC2981 + address royaltyRecipient = address(0x12345); + uint128 royaltyBps = 10; + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, royaltyBps); + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + + // 1. ========= Create listing ========= + + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); + + // 2. ========= Buy from listing ========= + + uint256 totalPrice = _buyFromListingForRoyaltyTests(listingId); + + // 3. ======== Check balances after royalty payments ======== + + { + uint256 platforfee = (platformFeeBps * totalPrice) / 10_000; + uint256 royaltyAmount = (royaltyBps * totalPrice) / 10_000; + + assertBalERC20Eq(address(erc20), platformFeeRecipient, platforfee); + + // Royalty recipient receives correct amounts + assertBalERC20Eq(address(erc20), royaltyRecipient, royaltyAmount); + + // Seller gets total price minus royalty amount minus platform fee + assertBalERC20Eq(address(erc20), seller, totalPrice - royaltyAmount - platforfee); + } + } + + function test_revert_mintra_native_royalty_feesExceedTotalPrice() public { + // Set native royalty too high + vm.prank(collectionOwner); + mintraDirectListingsLogicStandalone.createOrUpdateRoyalty(address(erc721Ownable), 10000, factoryAdmin); + + // 1. ========= Create listing ========= + erc721Ownable.mint(seller, 1); + uint256 listingId = _setupListingForRoyaltyTests(address(erc721Ownable)); + + // 2. ========= Buy from listing ========= + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.expectRevert("fees exceed the price"); + vm.prank(buyer); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_erc2981_royalty_feesExceedTotalPrice() public { + // Set erc2981 royalty too high + ERC721Base nft2981 = new ERC721Base(address(0x12345), "NFT 2981", "NFT2981", royaltyRecipient, 10000); + + // 1. ========= Create listing ========= + vm.prank(address(0x12345)); + nft2981.mintTo(seller, ""); + uint256 listingId = _setupListingForRoyaltyTests(address(nft2981)); + + // 2. ========= Buy from listing ========= + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.expectRevert("fees exceed the price"); + vm.prank(buyer); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + /*/////////////////////////////////////////////////////////////// + Create listing + //////////////////////////////////////////////////////////////*/ + + function createListing_1155(uint256 tokenId, uint256 totalListings) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = address(erc1155); + uint256 quantity = 2; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + erc1155.mint(seller, tokenId, quantity, ""); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = quantity; + + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc1155.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + listingId = MintraDirectListings(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Total listings incremented + assertEq(MintraDirectListings(marketplace).totalListings(), totalListings); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, startTimestamp); + assertEq(listing.endTimestamp, endTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC1155)); + + return listingId; + } + + function test_state_createListing() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + uint256 listingId = MintraDirectListings(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings incremented + assertEq(MintraDirectListings(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, startTimestamp); + assertEq(listing.endTimestamp, endTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_state_createListing_start_time_in_past() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + + vm.warp(10000); // Set the timestop for block 1 to 10000 + + uint256 expectedStartTimestamp = 10000; + uint256 expectedEndTimestamp = type(uint128).max; + // Set the start time to be at a timestamp in the past + uint128 startTimestamp = uint128(block.timestamp) - 1000; + + uint128 endTimestamp = type(uint128).max; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + uint256 listingId = MintraDirectListings(marketplace).createListing(listingParams); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings incremented + assertEq(MintraDirectListings(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, assetContract); + assertEq(listing.tokenId, tokenId); + assertEq(listing.quantity, quantity); + assertEq(listing.currency, currency); + assertEq(listing.pricePerToken, pricePerToken); + assertEq(listing.startTimestamp, expectedStartTimestamp); + assertEq(listing.endTimestamp, expectedEndTimestamp); + assertEq(listing.reserved, reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_revert_createListing_notOwnerOfListedToken() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Don't mint to 'token to be listed' to the seller. + address someWallet = getActor(1000); + _setupERC721BalanceForSeller(someWallet, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), someWallet, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(someWallet); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_revert_createListing_notApprovedMarketplaceToTransferToken() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingZeroQuantity() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 0; // Listing ZERO quantity + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingInvalidQuantity() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 2; // Listing more than `1` quantity + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: listing invalid quantity."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_revert_createListing_invalidStartTimestamp() public { + uint256 blockTimestamp = 100 minutes; + // Set block.timestamp + vm.warp(blockTimestamp); + + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = uint128(blockTimestamp - 61 minutes); // start time is less than block timestamp. + uint128 endTimestamp = uint128(startTimestamp + 1); + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid startTimestamp."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_revert_createListing_invalidEndTimestamp() public { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = uint128(startTimestamp - 1); // End timestamp is less than start timestamp. + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_revert_createListing_listingNonERC721OrERC1155Token() public { + // Sample listing parameters. + address assetContract = address(erc20); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); + MintraDirectListings(marketplace).createListing(listingParams); + } + + /*/////////////////////////////////////////////////////////////// + Update listing + //////////////////////////////////////////////////////////////*/ + + function _setup_updateListing( + uint256 tokenId + ) private returns (uint256 listingId, IDirectListings.ListingParameters memory listingParams) { + // listing parameters. + address assetContract = address(erc721); + uint256 quantity = 1; + address currency = address(erc20); + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = true; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(seller); + listingId = MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_state_updateListing() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.pricePerToken = 2 ether; + + vm.prank(seller); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings not incremented on update. + assertEq(MintraDirectListings(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, listingParamsToUpdate.assetContract); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, listingParamsToUpdate.quantity); + assertEq(listing.currency, listingParamsToUpdate.currency); + assertEq(listing.pricePerToken, listingParamsToUpdate.pricePerToken); + assertEq(listing.startTimestamp, listingParamsToUpdate.startTimestamp); + assertEq(listing.endTimestamp, listingParamsToUpdate.endTimestamp); + assertEq(listing.reserved, listingParamsToUpdate.reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_state_updateListing_start_time_in_past() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.pricePerToken = 2 ether; + + // Update the start time of the listing + uint256 expectedStartTimestamp = block.timestamp + 10; + uint256 expectedEndTimestamp = type(uint128).max; + + listingParamsToUpdate.startTimestamp = uint128(block.timestamp); + listingParamsToUpdate.endTimestamp = type(uint128).max; + vm.warp(block.timestamp + 10); // Set the timestamp 10 seconds in the future + + vm.prank(seller); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + + // Test consequent state of the contract. + + // Seller is still owner of token. + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Total listings not incremented on update. + assertEq(MintraDirectListings(marketplace).totalListings(), 1); + + // Fetch listing and verify state. + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + assertEq(listing.listingId, listingId); + assertEq(listing.listingCreator, seller); + assertEq(listing.assetContract, listingParamsToUpdate.assetContract); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, listingParamsToUpdate.quantity); + assertEq(listing.currency, listingParamsToUpdate.currency); + assertEq(listing.pricePerToken, listingParamsToUpdate.pricePerToken); + assertEq(listing.startTimestamp, expectedStartTimestamp); + assertEq(listing.endTimestamp, expectedEndTimestamp); + assertEq(listing.reserved, listingParamsToUpdate.reserved); + assertEq(uint256(listing.tokenType), uint256(IDirectListings.TokenType.ERC721)); + } + + function test_revert_updateListing_notListingCreator() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + address notSeller = getActor(1000); // Someone other than the seller calls update. + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_notOwnerOfListedToken() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens but NOT to seller. A new tokenId will be listed. + address notSeller = getActor(1000); + _setupERC721BalanceForSeller(notSeller, 1); + + // Approve Marketplace to transfer token. + vm.prank(notSeller); + erc721.setApprovalForAll(marketplace, true); + + // Transfer away owned token. + vm.prank(seller); + erc721.transferFrom(seller, address(0x1234), 0); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_notApprovedMarketplaceToTransferToken() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + // Don't approve Marketplace to transfer token. + vm.prank(seller); + erc721.setApprovalForAll(marketplace, false); + + vm.prank(seller); + vm.expectRevert("Marketplace: not owner or approved tokens."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingZeroQuantity() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.quantity = 0; // Listing zero quantity + + vm.prank(seller); + vm.expectRevert("Marketplace: listing zero quantity."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingInvalidQuantity() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.quantity = 2; // Listing more than `1` of the ERC721 token + + vm.prank(seller); + vm.expectRevert("Marketplace: listing invalid quantity."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_listingNonERC721OrERC1155Token() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + listingParamsToUpdate.assetContract = address(erc20); // Listing non ERC721 / ERC1155 token. + + vm.prank(seller); + vm.expectRevert("Marketplace: listed token must be ERC1155 or ERC721."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_invalidStartTimestamp() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + uint128 currentStartTimestamp = listingParamsToUpdate.startTimestamp; + listingParamsToUpdate.startTimestamp = currentStartTimestamp - 1; // Retroactively decreasing startTimestamp. + + vm.warp(currentStartTimestamp + 50); + vm.prank(seller); + vm.expectRevert("Marketplace: listing already active."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + function test_revert_updateListing_invalidEndTimestamp() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + + // Mint MORE ERC721 tokens to seller. A new tokenId will be listed. + _setupERC721BalanceForSeller(seller, 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + + uint128 currentStartTimestamp = listingParamsToUpdate.startTimestamp; + listingParamsToUpdate.endTimestamp = currentStartTimestamp - 1; // End timestamp less than startTimestamp + + vm.prank(seller); + vm.expectRevert("Marketplace: endTimestamp not greater than startTimestamp."); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + } + + /*/////////////////////////////////////////////////////////////// + Cancel listing + //////////////////////////////////////////////////////////////*/ + + function _setup_cancelListing( + uint256 tokenId + ) private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(tokenId); + listing = MintraDirectListings(marketplace).getListing(listingId); + } + + function test_state_cancelListing() public { + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(0); + + // Verify existing listing at `listingId` + assertEq(existingListingAtId.assetContract, address(erc721)); + + vm.prank(seller); + MintraDirectListings(marketplace).cancelListing(listingId); + + // status should be `CANCELLED` + IDirectListings.Listing memory cancelledListing = MintraDirectListings(marketplace).getListing( + listingId + ); + assertTrue(cancelledListing.status == IDirectListings.Status.CANCELLED); + } + + function test_revert_cancelListing_notListingCreator() public { + (uint256 listingId, IDirectListings.Listing memory existingListingAtId) = _setup_cancelListing(0); + + // Verify existing listing at `listingId` + assertEq(existingListingAtId.assetContract, address(erc721)); + + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListings(marketplace).cancelListing(listingId); + } + + function test_revert_cancelListing_nonExistentListing() public { + _setup_cancelListing(0); + + // Verify no listing exists at `nexListingId` + uint256 nextListingId = MintraDirectListings(marketplace).totalListings(); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid listing."); + MintraDirectListings(marketplace).cancelListing(nextListingId); + } + + /*/////////////////////////////////////////////////////////////// + Approve buyer for listing + //////////////////////////////////////////////////////////////*/ + + function _setup_approveBuyerForListing(uint256 tokenId) private returns (uint256 listingId) { + (listingId, ) = _setup_updateListing(tokenId); + } + + function test_state_approveBuyerForListing() public { + uint256 listingId = _setup_approveBuyerForListing(0); + bool toApprove = true; + + assertEq(MintraDirectListings(marketplace).getListing(listingId).reserved, true); + + // Seller approves buyer for reserved listing. + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + + assertEq(MintraDirectListings(marketplace).isBuyerApprovedForListing(listingId, buyer), true); + } + + function test_revert_approveBuyerForListing_notListingCreator() public { + uint256 listingId = _setup_approveBuyerForListing(0); + bool toApprove = true; + + assertEq(MintraDirectListings(marketplace).getListing(listingId).reserved, true); + + // Someone other than the seller approves buyer for reserved listing. + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + } + + function test_revert_approveBuyerForListing_listingNotReserved() public { + (uint256 listingId, IDirectListings.ListingParameters memory listingParamsToUpdate) = _setup_updateListing(0); + bool toApprove = true; + + assertEq(MintraDirectListings(marketplace).getListing(listingId).reserved, true); + + listingParamsToUpdate.reserved = false; + + vm.prank(seller); + MintraDirectListings(marketplace).updateListing(listingId, listingParamsToUpdate); + + assertEq(MintraDirectListings(marketplace).getListing(listingId).reserved, false); + + // Seller approves buyer for reserved listing. + vm.prank(seller); + vm.expectRevert("Marketplace: listing not reserved."); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, toApprove); + } + + /*/////////////////////////////////////////////////////////////// + Approve currency for listing + //////////////////////////////////////////////////////////////*/ + + function _setup_approveCurrencyForListing(uint256 tokenId) private returns (uint256 listingId) { + (listingId, ) = _setup_updateListing(tokenId); + } + + function test_state_approveCurrencyForListing() public { + uint256 listingId = _setup_approveCurrencyForListing(0); + address currencyToApprove = NATIVE_TOKEN; + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves buyer for reserved listing. + vm.prank(seller); + MintraDirectListings(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + + assertEq( + MintraDirectListings(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + true + ); + assertEq( + MintraDirectListings(marketplace).currencyPriceForListing(listingId, NATIVE_TOKEN), + pricePerTokenForCurrency + ); + } + + function test_revert_approveCurrencyForListing_notListingCreator() public { + uint256 listingId = _setup_approveCurrencyForListing(0); + address currencyToApprove = NATIVE_TOKEN; + uint256 pricePerTokenForCurrency = 2 ether; + + // Someone other than seller approves buyer for reserved listing. + address notSeller = getActor(1000); + vm.prank(notSeller); + vm.expectRevert("Marketplace: not listing creator."); + MintraDirectListings(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + } + + function test_revert_approveCurrencyForListing_reApprovingMainCurrency() public { + uint256 listingId = _setup_approveCurrencyForListing(0); + address currencyToApprove = MintraDirectListings(marketplace).getListing(listingId).currency; + uint256 pricePerTokenForCurrency = 2 ether; + + // Seller approves buyer for reserved listing. + vm.prank(seller); + vm.expectRevert("Marketplace: approving listing currency with different price."); + MintraDirectListings(marketplace).approveCurrencyForListing( + listingId, + currencyToApprove, + pricePerTokenForCurrency + ); + } + + /*/////////////////////////////////////////////////////////////// + Buy from listing + //////////////////////////////////////////////////////////////*/ + + function _setup_buyFromListing( + uint256 tokenId + ) private returns (uint256 listingId, IDirectListings.Listing memory listing) { + (listingId, ) = _setup_updateListing(tokenId); + listing = MintraDirectListings(marketplace).getListing(listingId); + } + + function test_state_buyFromListing_with_mint_token() public { + uint256 listingId = _createListing(seller, address(erc20Aux)); + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBpsMint = MintraDirectListings(marketplace).platformFeeBpsMint(); + uint256 platformFee = (totalPrice * platformFeeBpsMint) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20Aux.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20Aux), buyer, totalPrice); + assertBalERC20Eq(address(erc20Aux), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20Aux.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20Aux), buyer, 0); + assertBalERC20Eq(address(erc20Aux), seller, totalPrice - platformFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = MintraDirectListings(marketplace) + .getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_state_buyFromListing_721() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = MintraDirectListings(marketplace) + .getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_state_buyFromListing_multi_721() public { + vm.prank(seller); + (uint256 listingIdOne, IDirectListings.Listing memory listingOne) = _setup_buyFromListing(0); + vm.prank(seller); + (uint256 listingIdTwo, IDirectListings.Listing memory listingTwo) = _setup_buyFromListing(1); + + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingIdOne, buyer, true); + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingIdTwo, buyer, true); + + address buyFor = buyer; + uint256 quantityToBuy = listingOne.quantity; + address currency = listingOne.currency; + uint256 pricePerToken = listingOne.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice + totalPrice); + + // Buy tokens from listing. + vm.warp(listingTwo.startTimestamp); + { + uint256[] memory listingIdArray = new uint256[](2); + listingIdArray[0] = listingIdOne; + listingIdArray[1] = listingIdTwo; + + address[] memory buyForArray = new address[](2); + buyForArray[0] = buyFor; + buyForArray[1] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](2); + quantityToBuyArray[0] = quantityToBuy; + quantityToBuyArray[1] = quantityToBuy; + + address[] memory currencyArray = new address[](2); + currencyArray[0] = currency; + currencyArray[1] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](2); + expectedTotalPriceArray[0] = totalPrice; + expectedTotalPriceArray[1] = totalPrice; + + vm.prank(buyer); + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + uint256 sellerPayout = totalPrice + totalPrice - platformFee - platformFee; + assertBalERC20Eq(address(erc20), seller, sellerPayout); + } + + function test_state_buyFromListing_1155() public { + // Create the listing + uint256 listingId = createListing_1155(0, 1); + + IDirectListings.Listing memory listing = MintraDirectListings(marketplace).getListing(listingId); + + address buyFor = buyer; + uint256 tokenId = listing.tokenId; + uint256 quantity = listing.quantity; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = quantity; + + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertBalERC1155Eq(address(erc1155), buyer, tokenIds, amounts); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFee); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = MintraDirectListings(marketplace) + .getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_state_buyFromListing_multi_1155() public { + vm.prank(seller); + uint256 listingIdOne = createListing_1155(0, 1); + IDirectListings.Listing memory listingOne = MintraDirectListings(marketplace).getListing( + listingIdOne + ); + + vm.prank(seller); + uint256 listingIdTwo = createListing_1155(1, 2); + IDirectListings.Listing memory listingTwo = MintraDirectListings(marketplace).getListing( + listingIdTwo + ); + + address buyFor = buyer; + uint256 quantityToBuy = listingOne.quantity; + address currency = listingOne.currency; + uint256 pricePerToken = listingOne.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 0; + tokenIds[1] = 1; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 2; + amounts[1] = 2; + + assertBalERC1155Eq(address(erc1155), seller, tokenIds, amounts); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice + totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice + totalPrice); + + // Buy tokens from listing. + vm.warp(listingTwo.startTimestamp); + { + uint256[] memory listingIdArray = new uint256[](2); + listingIdArray[0] = listingIdOne; + listingIdArray[1] = listingIdTwo; + + address[] memory buyForArray = new address[](2); + buyForArray[0] = buyFor; + buyForArray[1] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](2); + quantityToBuyArray[0] = quantityToBuy; + quantityToBuyArray[1] = quantityToBuy; + + address[] memory currencyArray = new address[](2); + currencyArray[0] = currency; + currencyArray[1] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](2); + expectedTotalPriceArray[0] = totalPrice; + expectedTotalPriceArray[1] = totalPrice; + + vm.prank(buyer); + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertBalERC1155Eq(address(erc1155), buyer, tokenIds, amounts); + + // Verify seller is paid total price. + assertBalERC20Eq(address(erc20), buyer, 0); + uint256 sellerPayout = totalPrice + totalPrice - platformFee - platformFee; + assertBalERC20Eq(address(erc20), seller, sellerPayout); + } + + function test_state_bulkBuyFromListing_nativeToken() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + uint256 platformFee = (totalPrice * platformFeeBps) / 10000; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + uint256 buyerBalBefore = buyer.balance; + uint256 sellerBalBefore = seller.balance; + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing{ value: totalPrice }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + // Verify that buyer is owner of listed tokens, post-sale. + assertIsOwnerERC721(address(erc721), buyer, tokenIds); + assertIsNotOwnerERC721(address(erc721), seller, tokenIds); + + // Verify seller is paid total price. + assertEq(buyer.balance, buyerBalBefore - totalPrice); + assertEq(seller.balance, sellerBalBefore + (totalPrice - platformFee)); + + if (quantityToBuy == listing.quantity) { + // Verify listing status is `COMPLETED` if listing tokens are all bought. + IDirectListings.Listing memory completedListing = MintraDirectListings(marketplace) + .getListing(listingId); + assertTrue(completedListing.status == IDirectListings.Status.COMPLETED); + } + } + + function test_revert_bulkBuyFromListing_nativeToken_incorrectValueSent() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("native token transfer failed"); + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing{ value: totalPrice - 1 }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + } + + function test_revert_buyFromListing_unexpectedTotalPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = NATIVE_TOKEN; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Approve NATIVE_TOKEN for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveCurrencyForListing(listingId, currency, pricePerToken); + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Deal requisite total price to buyer. + vm.deal(buyer, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Unexpected total price"); + + { + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice + 1; + + MintraDirectListings(marketplace).bulkBuyFromListing{ value: totalPrice - 1 }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + } + + function test_revert_buyFromListing_invalidCurrency() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + + assertEq(listing.currency, address(erc20)); + assertEq( + MintraDirectListings(marketplace).isCurrencyApprovedForListing(listingId, NATIVE_TOKEN), + false + ); + + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Paying in invalid currency."); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = NATIVE_TOKEN; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_buyerBalanceLessThanPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice - 1); // Buyer balance less than total price + assertBalERC20Eq(address(erc20), buyer, totalPrice - 1); + assertBalERC20Eq(address(erc20), seller, 0); + + // Approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_notApprovedMarketplaceToTransferPrice() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity; + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.approve(marketplace, 0); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("!BAL20"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_buyingZeroQuantity() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = 0; // Buying zero quantity + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + function test_revert_buyFromListing_buyingMoreQuantityThanListed() public { + (uint256 listingId, IDirectListings.Listing memory listing) = _setup_buyFromListing(0); + + address buyFor = buyer; + uint256 quantityToBuy = listing.quantity + 1; // Buying more than listed. + address currency = listing.currency; + uint256 pricePerToken = listing.pricePerToken; + uint256 totalPrice = pricePerToken * quantityToBuy; + + // Seller approves buyer for listing + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + // Verify that seller is owner of listed tokens, pre-sale. + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + assertIsOwnerERC721(address(erc721), seller, tokenIds); + assertIsNotOwnerERC721(address(erc721), buyer, tokenIds); + + // Mint requisite total price to buyer. + erc20.mint(buyer, totalPrice); + assertBalERC20Eq(address(erc20), buyer, totalPrice); + assertBalERC20Eq(address(erc20), seller, 0); + + // Don't approve marketplace to transfer currency + vm.prank(buyer); + erc20.increaseAllowance(marketplace, totalPrice); + + // Buy tokens from listing. + vm.warp(listing.startTimestamp); + vm.prank(buyer); + vm.expectRevert("Buying invalid quantity"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyFor; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = quantityToBuy; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = currency; + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = totalPrice; + + MintraDirectListings(marketplace).bulkBuyFromListing( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + function test_getAllListing() public { + // Create the listing + createListing_1155(0, 1); + + IDirectListings.Listing[] memory listings = MintraDirectListings(marketplace).getAllListings( + 0, + 0 + ); + + assertEq(listings.length, 1); + + IDirectListings.Listing memory listing = listings[0]; + + assertEq(listing.assetContract, address(erc1155)); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, 2); + assertEq(listing.currency, address(erc20)); + assertEq(listing.pricePerToken, 1 ether); + assertEq(listing.startTimestamp, 100); + assertEq(listing.endTimestamp, 200); + assertEq(listing.reserved, false); + } + + function test_getAllValidListings() public { + // Create the listing + createListing_1155(0, 1); + + IDirectListings.Listing[] memory listingsAll = MintraDirectListings(marketplace).getAllListings( + 0, + 0 + ); + + assertEq(listingsAll.length, 1); + + vm.warp(listingsAll[0].startTimestamp); + IDirectListings.Listing[] memory listings = MintraDirectListings(marketplace) + .getAllValidListings(0, 0); + + assertEq(listings.length, 1); + + IDirectListings.Listing memory listing = listings[0]; + + assertEq(listing.assetContract, address(erc1155)); + assertEq(listing.tokenId, 0); + assertEq(listing.quantity, 2); + assertEq(listing.currency, address(erc20)); + assertEq(listing.pricePerToken, 1 ether); + assertEq(listing.startTimestamp, 100); + assertEq(listing.endTimestamp, 200); + assertEq(listing.reserved, false); + } + + function test_currencyPriceForListing_fail() public { + // Create the listing + createListing_1155(0, 1); + + vm.expectRevert("Currency not approved for listing"); + MintraDirectListings(marketplace).currencyPriceForListing(0, address(erc20Aux)); + } + + function _createListing(address _seller, address currency) private returns (uint256 listingId) { + // Sample listing parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + uint256 pricePerToken = 1 ether; + uint128 startTimestamp = 100; + uint128 endTimestamp = 200; + bool reserved = false; + + // Mint the ERC721 tokens to seller. These tokens will be listed. + _setupERC721BalanceForSeller(_seller, 1); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + assertIsOwnerERC721(address(erc721), _seller, tokenIds); + + // Approve Marketplace to transfer token. + vm.prank(_seller); + erc721.setApprovalForAll(marketplace, true); + + // List tokens. + IDirectListings.ListingParameters memory listingParams = IDirectListings.ListingParameters( + assetContract, + tokenId, + quantity, + currency, + pricePerToken, + startTimestamp, + endTimestamp, + reserved + ); + + vm.prank(_seller); + listingId = MintraDirectListings(marketplace).createListing(listingParams); + } + + function test_audit_native_tokens_locked() public { + (uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing(0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = existingListing.tokenId; + + // Verify existing auction at `auctionId` + assertEq(existingListing.assetContract, address(erc721)); + + vm.warp(existingListing.startTimestamp); + + // No ether is locked in contract + assertEq(marketplace.balance, 0); + + // buy from listing + erc20.mint(buyer, 10 ether); + vm.deal(buyer, 1 ether); + + vm.prank(seller); + MintraDirectListings(marketplace).approveBuyerForListing(listingId, buyer, true); + + vm.startPrank(buyer); + erc20.approve(marketplace, 10 ether); + + vm.expectRevert("Incorrect PLS amount sent"); + + uint256[] memory listingIdArray = new uint256[](1); + listingIdArray[0] = listingId; + + address[] memory buyForArray = new address[](1); + buyForArray[0] = buyer; + + uint256[] memory quantityToBuyArray = new uint256[](1); + quantityToBuyArray[0] = 1; + + address[] memory currencyArray = new address[](1); + currencyArray[0] = address(erc20); + + uint256[] memory expectedTotalPriceArray = new uint256[](1); + expectedTotalPriceArray[0] = 1 ether; + + MintraDirectListings(marketplace).bulkBuyFromListing{ value: 1 ether }( + listingIdArray, + buyForArray, + quantityToBuyArray, + currencyArray, + expectedTotalPriceArray + ); + + vm.stopPrank(); + + // 1 ether is temporary locked in contract + assertEq(marketplace.balance, 0 ether); + } + + function test_set_platform_fee() public { + uint256 platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + assertEq(platformFeeBps, 225); + + vm.prank(wizard); + MintraDirectListings(marketplace).setPlatformFeeBps(369); + + platformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + + assertEq(platformFeeBps, 369); + } + + function test_fuzz_set_platform_fee(uint256 platformFeeBps) public { + vm.assume(platformFeeBps <= 369); + + vm.prank(wizard); + MintraDirectListings(marketplace).setPlatformFeeBps(platformFeeBps); + + uint256 expectedPlatformFeeBps = MintraDirectListings(marketplace).platformFeeBps(); + + assertEq(expectedPlatformFeeBps, platformFeeBps); + } + + function test_set_platform_fee_fail() public { + vm.prank(wizard); + vm.expectRevert("Fee not in range"); + MintraDirectListings(marketplace).setPlatformFeeBps(1000); + } +}