From 32d17b06b1f5df0abe376fb93be12258f6707a8a Mon Sep 17 00:00:00 2001 From: Ignacio Mazzara Date: Thu, 6 Jan 2022 11:02:43 -0500 Subject: [PATCH] Feat: royalties (#56) * wip: royalties * wip: fallback on creator * feat: use soliditiy 0.8 * wip: make old test pass * feat: test royalties; * feat: enhance error messages * feat: add missing test cases * feat: compute royalties * refactor: unused var * feat: add more tests * feat: send fees on publication fees * feat: add deployers * feat: deploy with priv * fix: tests --- contracts/commons/ContextMixin.sol | 4 +- contracts/commons/EIP712Base.sol | 4 +- contracts/commons/NativeMetaTransaction.sol | 4 +- contracts/commons/Ownable.sol | 4 +- contracts/commons/Pausable.sol | 4 +- contracts/interfaces/IERC721CollectionV2.sol | 11 + contracts/interfaces/IERC721Verifiable.sol | 10 + contracts/interfaces/IRoyaltiesManager.sol | 7 + contracts/managers/RoyaltiesManager.sol | 65 + contracts/marketplace/Marketplace.sol | 10 +- contracts/marketplace/MarketplaceStorage.sol | 4 +- contracts/marketplace/MarketplaceV2.sol | 477 ++++ contracts/mocks/ERC20Test.sol | 2 + contracts/mocks/ERC721Test.sol | 8 +- contracts/mocks/ERC721TestCollection.sol | 45 + contracts/mocks/MarketplaceTest.sol | 2 + contracts/mocks/VerifiableERC721Test.sol | 4 +- full/Marketplace.sol | 365 +-- full/MarketplaceV2.sol | 1344 +++++++++ hardhat.config.ts | 12 + package-lock.json | 85 +- package.json | 4 +- scripts/DeployRoyaltiesManager.ts | 36 + scripts/buildfull.sh | 3 + scripts/deployMarketplaceV2.ts | 70 + scripts/utils.ts | 5 +- scripts/verifyContract.ts | 22 + test/MarketplaceV2.js | 2678 ++++++++++++++++++ test/helpers/assertRevert.js | 16 + 29 files changed, 5038 insertions(+), 267 deletions(-) create mode 100644 contracts/interfaces/IERC721CollectionV2.sol create mode 100644 contracts/interfaces/IERC721Verifiable.sol create mode 100644 contracts/interfaces/IRoyaltiesManager.sol create mode 100644 contracts/managers/RoyaltiesManager.sol create mode 100644 contracts/marketplace/MarketplaceV2.sol create mode 100644 contracts/mocks/ERC721TestCollection.sol create mode 100644 full/MarketplaceV2.sol create mode 100644 scripts/DeployRoyaltiesManager.ts create mode 100644 scripts/deployMarketplaceV2.ts create mode 100644 scripts/verifyContract.ts create mode 100644 test/MarketplaceV2.js create mode 100644 test/helpers/assertRevert.js diff --git a/contracts/commons/ContextMixin.sol b/contracts/commons/ContextMixin.sol index f8101ee..81857dd 100644 --- a/contracts/commons/ContextMixin.sol +++ b/contracts/commons/ContextMixin.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; contract ContextMixin { diff --git a/contracts/commons/EIP712Base.sol b/contracts/commons/EIP712Base.sol index 176c429..21ccc8b 100644 --- a/contracts/commons/EIP712Base.sol +++ b/contracts/commons/EIP712Base.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; contract EIP712Base { diff --git a/contracts/commons/NativeMetaTransaction.sol b/contracts/commons/NativeMetaTransaction.sol index 4f6ea85..e37d297 100644 --- a/contracts/commons/NativeMetaTransaction.sol +++ b/contracts/commons/NativeMetaTransaction.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; import { EIP712Base } from "./EIP712Base.sol"; diff --git a/contracts/commons/Ownable.sol b/contracts/commons/Ownable.sol index c427fb3..6d1bb31 100644 --- a/contracts/commons/Ownable.sol +++ b/contracts/commons/Ownable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.8.0; +pragma solidity >=0.6.0; import "./ContextMixin.sol"; @@ -24,7 +24,7 @@ abstract contract Ownable is ContextMixin { /** * @dev Initializes the contract setting the deployer as the initial owner. */ - constructor () internal { + constructor () { address msgSender = _msgSender(); _owner = msgSender; emit OwnershipTransferred(address(0), msgSender); diff --git a/contracts/commons/Pausable.sol b/contracts/commons/Pausable.sol index 623ccc8..c582ea8 100644 --- a/contracts/commons/Pausable.sol +++ b/contracts/commons/Pausable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.8.0; +pragma solidity >=0.6.0; import "./ContextMixin.sol"; @@ -29,7 +29,7 @@ abstract contract Pausable is ContextMixin { /** * @dev Initializes the contract in unpaused state. */ - constructor () internal { + constructor () { _paused = false; } diff --git a/contracts/interfaces/IERC721CollectionV2.sol b/contracts/interfaces/IERC721CollectionV2.sol new file mode 100644 index 0000000..9701928 --- /dev/null +++ b/contracts/interfaces/IERC721CollectionV2.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; +pragma experimental ABIEncoderV2; + + +interface IERC721CollectionV2 { + function creator() external view returns (address); + function decodeTokenId(uint256 _tokenId) external view returns (uint256, uint256); + function items(uint256 _itemId) external view returns (string memory, uint256, uint256, uint256, address, string memory, string memory); +} \ No newline at end of file diff --git a/contracts/interfaces/IERC721Verifiable.sol b/contracts/interfaces/IERC721Verifiable.sol new file mode 100644 index 0000000..b9ecb49 --- /dev/null +++ b/contracts/interfaces/IERC721Verifiable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + + +interface IERC721Verifiable is IERC721 { + function verifyFingerprint(uint256, bytes memory) external view returns (bool); +} diff --git a/contracts/interfaces/IRoyaltiesManager.sol b/contracts/interfaces/IRoyaltiesManager.sol new file mode 100644 index 0000000..4133ede --- /dev/null +++ b/contracts/interfaces/IRoyaltiesManager.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +interface IRoyaltiesManager { + function getRoyaltiesReceiver(address _contractAddress, uint256 _tokenId) external view returns (address); +} diff --git a/contracts/managers/RoyaltiesManager.sol b/contracts/managers/RoyaltiesManager.sol new file mode 100644 index 0000000..116382b --- /dev/null +++ b/contracts/managers/RoyaltiesManager.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + + +import '../interfaces/IERC721CollectionV2.sol'; + + +contract RoyaltiesManager{ + + constructor() {} + + /** + * @notice Get the royalties receiver for an specific token + * @dev It tries to get the item beneficiary. If it is the ZERO address, will try to get the creator + * @param _contractAddress - contract address + * @param _tokenId - token id + * @return royaltiesReceiver - address of the royalties receiver + */ + function getRoyaltiesReceiver(address _contractAddress, uint256 _tokenId) external view returns(address royaltiesReceiver) { + bool success; + bytes memory res; + + (success, res) = _contractAddress.staticcall( + abi.encodeWithSelector( + IERC721CollectionV2(_contractAddress).decodeTokenId.selector, + _tokenId + ) + ); + + if (!success) { + return royaltiesReceiver; + } + + (uint256 itemId,) = abi.decode(res, (uint256, uint256)); + + (success, res) = _contractAddress.staticcall( + abi.encodeWithSelector( + IERC721CollectionV2(_contractAddress).items.selector, + itemId + ) + ); + + if (success) { + // Get item beneficiary + (,,,,royaltiesReceiver,,) = abi.decode(res, (string, uint256, uint256, uint256, address, string, string)); + } + + if (royaltiesReceiver == address(0)) { + // If still the zero address, use the creator + (success, res) = _contractAddress.staticcall( + abi.encodeWithSelector( + IERC721CollectionV2(_contractAddress).creator.selector + )); + + if (!success) { + return royaltiesReceiver; + } + + royaltiesReceiver = abi.decode(res, (address)); + } + + return royaltiesReceiver; + } +} diff --git a/contracts/marketplace/Marketplace.sol b/contracts/marketplace/Marketplace.sol index 5e6bced..35c3e92 100644 --- a/contracts/marketplace/Marketplace.sol +++ b/contracts/marketplace/Marketplace.sol @@ -1,6 +1,8 @@ -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT -import "@openzeppelin/contracts/math/SafeMath.sol"; +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "./MarketplaceStorage.sol"; @@ -24,9 +26,7 @@ contract Marketplace is Ownable, Pausable, MarketplaceStorage, NativeMetaTransac address _acceptedToken, uint256 _ownerCutPerMillion, address _owner - ) - public - { + ) { // EIP712 init _initializeEIP712('Decentraland Marketplace', '1'); diff --git a/contracts/marketplace/MarketplaceStorage.sol b/contracts/marketplace/MarketplaceStorage.sol index 07753d4..8c272ba 100644 --- a/contracts/marketplace/MarketplaceStorage.sol +++ b/contracts/marketplace/MarketplaceStorage.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; /** diff --git a/contracts/marketplace/MarketplaceV2.sol b/contracts/marketplace/MarketplaceV2.sol new file mode 100644 index 0000000..d03c33f --- /dev/null +++ b/contracts/marketplace/MarketplaceV2.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "../commons/Ownable.sol"; +import "../commons/Pausable.sol"; +import "../commons/ContextMixin.sol"; +import "../commons/NativeMetaTransaction.sol"; +import "../interfaces/IERC721Verifiable.sol"; +import "../interfaces/IRoyaltiesManager.sol"; + + + +contract MarketplaceV2 is Ownable, Pausable, NativeMetaTransaction { + using Address for address; + + IERC20 public acceptedToken; + + struct Order { + // Order ID + bytes32 id; + // Owner of the NFT + address seller; + // NFT registry address + address nftAddress; + // Price (in wei) for the published item + uint256 price; + // Time when this sale ends + uint256 expiresAt; + } + + // From ERC721 registry assetId to Order (to avoid asset collision) + mapping (address => mapping(uint256 => Order)) public orderByAssetId; + + address public feesCollector; + IRoyaltiesManager public royaltiesManager; + + uint256 public feesCollectorCutPerMillion; + uint256 public royaltiesCutPerMillion; + uint256 public publicationFeeInWei; + + + bytes4 public constant InterfaceId_ValidateFingerprint = bytes4( + keccak256("verifyFingerprint(uint256,bytes)") + ); + + bytes4 public constant ERC721_Interface = bytes4(0x80ac58cd); + + // EVENTS + event OrderCreated( + bytes32 id, + uint256 indexed assetId, + address indexed seller, + address nftAddress, + uint256 priceInWei, + uint256 expiresAt + ); + event OrderSuccessful( + bytes32 id, + uint256 indexed assetId, + address indexed seller, + address nftAddress, + uint256 totalPrice, + address indexed buyer + ); + event OrderCancelled( + bytes32 id, + uint256 indexed assetId, + address indexed seller, + address nftAddress + ); + + event ChangedPublicationFee(uint256 publicationFee); + event ChangedFeesCollectorCutPerMillion(uint256 feesCollectorCutPerMillion); + event ChangedRoyaltiesCutPerMillion(uint256 royaltiesCutPerMillion); + event FeesCollectorSet(address indexed oldFeesCollector, address indexed newFeesCollector); + event RoyaltiesManagerSet(IRoyaltiesManager indexed oldRoyaltiesManager, IRoyaltiesManager indexed newRoyaltiesManager); + + + /** + * @dev Initialize this contract. Acts as a constructor + * @param _owner - owner + * @param _feesCollector - fees collector + * @param _acceptedToken - Address of the ERC20 accepted for this marketplace + * @param _royaltiesManager - Royalties manager contract + * @param _feesCollectorCutPerMillion - fees collector cut per million + * @param _royaltiesCutPerMillion - royalties cut per million + */ + constructor ( + address _owner, + address _feesCollector, + address _acceptedToken, + IRoyaltiesManager _royaltiesManager, + uint256 _feesCollectorCutPerMillion, + uint256 _royaltiesCutPerMillion + ) { + // EIP712 init + _initializeEIP712('Decentraland Marketplace', '2'); + + // Address init + setFeesCollector(_feesCollector); + setRoyaltiesManager(_royaltiesManager); + + // Fee init + setFeesCollectorCutPerMillion(_feesCollectorCutPerMillion); + setRoyaltiesCutPerMillion(_royaltiesCutPerMillion); + + require(_owner != address(0), "MarketplaceV2#constructor: INVALID_OWNER"); + transferOwnership(_owner); + + require(_acceptedToken.isContract(), "MarketplaceV2#constructor: INVALID_ACCEPTED_TOKEN"); + acceptedToken = IERC20(_acceptedToken); + } + + + /** + * @dev Sets the publication fee that's charged to users to publish items + * @param _publicationFee - Fee amount in wei this contract charges to publish an item + */ + function setPublicationFee(uint256 _publicationFee) external onlyOwner { + publicationFeeInWei = _publicationFee; + emit ChangedPublicationFee(publicationFeeInWei); + } + + /** + * @dev Sets the share cut for the fees collector of the contract that's + * charged to the seller on a successful sale + * @param _feesCollectorCutPerMillion - fees for the collector + */ + function setFeesCollectorCutPerMillion(uint256 _feesCollectorCutPerMillion) public onlyOwner { + feesCollectorCutPerMillion = _feesCollectorCutPerMillion; + + require( + feesCollectorCutPerMillion + royaltiesCutPerMillion < 1000000, + "MarketplaceV2#setFeesCollectorCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999" + ); + + emit ChangedFeesCollectorCutPerMillion(feesCollectorCutPerMillion); + } + + /** + * @dev Sets the share cut for the royalties that's + * charged to the seller on a successful sale + * @param _royaltiesCutPerMillion - fees for royalties + */ + function setRoyaltiesCutPerMillion(uint256 _royaltiesCutPerMillion) public onlyOwner { + royaltiesCutPerMillion = _royaltiesCutPerMillion; + + require( + feesCollectorCutPerMillion + royaltiesCutPerMillion < 1000000, + "MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999" + ); + + emit ChangedRoyaltiesCutPerMillion(royaltiesCutPerMillion); + } + + /** + * @notice Set the fees collector + * @param _newFeesCollector - fees collector + */ + function setFeesCollector(address _newFeesCollector) onlyOwner public { + require(_newFeesCollector != address(0), "MarketplaceV2#setFeesCollector: INVALID_FEES_COLLECTOR"); + + emit FeesCollectorSet(feesCollector, _newFeesCollector); + feesCollector = _newFeesCollector; + } + + /** + * @notice Set the royalties manager + * @param _newRoyaltiesManager - royalties manager + */ + function setRoyaltiesManager(IRoyaltiesManager _newRoyaltiesManager) onlyOwner public { + require(address(_newRoyaltiesManager).isContract(), "MarketplaceV2#setRoyaltiesManager: INVALID_ROYALTIES_MANAGER"); + + + emit RoyaltiesManagerSet(royaltiesManager, _newRoyaltiesManager); + royaltiesManager = _newRoyaltiesManager; + } + + + /** + * @dev Creates a new order + * @param nftAddress - Non fungible registry address + * @param assetId - ID of the published NFT + * @param priceInWei - Price in Wei for the supported coin + * @param expiresAt - Duration of the order (in hours) + */ + function createOrder( + address nftAddress, + uint256 assetId, + uint256 priceInWei, + uint256 expiresAt + ) + public + whenNotPaused + { + _createOrder( + nftAddress, + assetId, + priceInWei, + expiresAt + ); + } + + /** + * @dev Cancel an already published order + * can only be canceled by seller or the contract owner + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + */ + function cancelOrder(address nftAddress, uint256 assetId) public whenNotPaused { + _cancelOrder(nftAddress, assetId); + } + + /** + * @dev Executes the sale for a published NFT and checks for the asset fingerprint + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + * @param price - Order price + * @param fingerprint - Verification info for the asset + */ + function safeExecuteOrder( + address nftAddress, + uint256 assetId, + uint256 price, + bytes memory fingerprint + ) + public + whenNotPaused + { + _executeOrder( + nftAddress, + assetId, + price, + fingerprint + ); + } + + /** + * @dev Executes the sale for a published NFT + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + * @param price - Order price + */ + function executeOrder( + address nftAddress, + uint256 assetId, + uint256 price + ) + public + whenNotPaused + { + _executeOrder( + nftAddress, + assetId, + price, + "" + ); + } + + /** + * @dev Creates a new order + * @param nftAddress - Non fungible registry address + * @param assetId - ID of the published NFT + * @param priceInWei - Price in Wei for the supported coin + * @param expiresAt - Duration of the order (in hours) + */ + function _createOrder( + address nftAddress, + uint256 assetId, + uint256 priceInWei, + uint256 expiresAt + ) + internal + { + _requireERC721(nftAddress); + + address sender = _msgSender(); + + IERC721Verifiable nftRegistry = IERC721Verifiable(nftAddress); + address assetOwner = nftRegistry.ownerOf(assetId); + + require(sender == assetOwner, "MarketplaceV2#_createOrder: NOT_ASSET_OWNER"); + require( + nftRegistry.getApproved(assetId) == address(this) || nftRegistry.isApprovedForAll(assetOwner, address(this)), + "The contract is not authorized to manage the asset" + ); + require(priceInWei > 0, "Price should be bigger than 0"); + require(expiresAt > block.timestamp + 1 minutes, "MarketplaceV2#_createOrder: INVALID_EXPIRES_AT"); + + bytes32 orderId = keccak256( + abi.encodePacked( + block.timestamp, + assetOwner, + assetId, + nftAddress, + priceInWei + ) + ); + + orderByAssetId[nftAddress][assetId] = Order({ + id: orderId, + seller: assetOwner, + nftAddress: nftAddress, + price: priceInWei, + expiresAt: expiresAt + }); + + // Check if there's a publication fee and + // transfer the amount to marketplace owner + if (publicationFeeInWei > 0) { + require( + acceptedToken.transferFrom(sender, feesCollector, publicationFeeInWei), + "MarketplaceV2#_createOrder: TRANSFER_FAILED" + ); + } + + emit OrderCreated( + orderId, + assetId, + assetOwner, + nftAddress, + priceInWei, + expiresAt + ); + } + + /** + * @dev Cancel an already published order + * can only be canceled by seller or the contract owner + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + */ + function _cancelOrder(address nftAddress, uint256 assetId) internal returns (Order memory) { + address sender = _msgSender(); + Order memory order = orderByAssetId[nftAddress][assetId]; + + require(order.id != 0, "MarketplaceV2#_cancelOrder: INVALID_ORDER"); + require(order.seller == sender || sender == owner(), "MarketplaceV2#_cancelOrder: UNAUTHORIZED_USER"); + + bytes32 orderId = order.id; + address orderSeller = order.seller; + address orderNftAddress = order.nftAddress; + delete orderByAssetId[nftAddress][assetId]; + + emit OrderCancelled( + orderId, + assetId, + orderSeller, + orderNftAddress + ); + + return order; + } + + /** + * @dev Executes the sale for a published NFT + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + * @param price - Order price + * @param fingerprint - Verification info for the asset + */ + function _executeOrder( + address nftAddress, + uint256 assetId, + uint256 price, + bytes memory fingerprint + ) + internal returns (Order memory) + { + _requireERC721(nftAddress); + + address sender = _msgSender(); + + IERC721Verifiable nftRegistry = IERC721Verifiable(nftAddress); + + if (nftRegistry.supportsInterface(InterfaceId_ValidateFingerprint)) { + require( + nftRegistry.verifyFingerprint(assetId, fingerprint), + "MarketplaceV2#_executeOrder: INVALID_FINGERPRINT" + ); + } + Order memory order = orderByAssetId[nftAddress][assetId]; + + require(order.id != 0, "MarketplaceV2#_executeOrder: ASSET_NOT_FOR_SALE"); + + require(order.seller != address(0), "MarketplaceV2#_executeOrder: INVALID_SELLER"); + require(order.seller != sender, "MarketplaceV2#_executeOrder: SENDER_IS_SELLER"); + require(order.price == price, "MarketplaceV2#_executeOrder: PRICE_MISMATCH"); + require(block.timestamp < order.expiresAt, "MarketplaceV2#_executeOrder: ORDER_EXPIRED"); + require(order.seller == nftRegistry.ownerOf(assetId), "MarketplaceV2#_executeOrder: SELLER_NOT_OWNER"); + + + delete orderByAssetId[nftAddress][assetId]; + + uint256 feesCollectorShareAmount; + uint256 royaltiesShareAmount; + address royaltiesReceiver; + + // Royalties share + if (royaltiesCutPerMillion > 0) { + royaltiesShareAmount = (price * royaltiesCutPerMillion) / 1000000; + + (bool success, bytes memory res) = address(royaltiesManager).staticcall( + abi.encodeWithSelector( + royaltiesManager.getRoyaltiesReceiver.selector, + address(nftRegistry), + assetId + ) + ); + + if (success) { + (royaltiesReceiver) = abi.decode(res, (address)); + if (royaltiesReceiver != address(0)) { + require( + acceptedToken.transferFrom(sender, royaltiesReceiver, royaltiesShareAmount), + "MarketplaceV2#_executeOrder: TRANSFER_FEES_TO_ROYALTIES_RECEIVER_FAILED" + ); + } + } + } + + // Fees collector share + { + feesCollectorShareAmount = (price * feesCollectorCutPerMillion) / 1000000; + uint256 totalFeeCollectorShareAmount = feesCollectorShareAmount; + + if (royaltiesShareAmount > 0 && royaltiesReceiver == address(0)) { + totalFeeCollectorShareAmount += royaltiesShareAmount; + } + + if (totalFeeCollectorShareAmount > 0) { + require( + acceptedToken.transferFrom(sender, feesCollector, totalFeeCollectorShareAmount), + "MarketplaceV2#_executeOrder: TRANSFER_FEES_TO_FEES_COLLECTOR_FAILED" + ); + } + } + + // Transfer sale amount to seller + require( + acceptedToken.transferFrom(sender, order.seller, price - royaltiesShareAmount - feesCollectorShareAmount), + "MarketplaceV2#_executeOrder: TRANSFER_AMOUNT_TO_SELLER_FAILED" + ); + + // Transfer asset owner + nftRegistry.safeTransferFrom( + order.seller, + sender, + assetId + ); + + emit OrderSuccessful( + order.id, + assetId, + order.seller, + nftAddress, + price, + sender + ); + + return order; + } + + function _requireERC721(address nftAddress) internal view { + require(nftAddress.isContract(), "MarketplaceV2#_requireERC721: INVALID_NFT_ADDRESS"); + + IERC721 nftRegistry = IERC721(nftAddress); + require( + nftRegistry.supportsInterface(ERC721_Interface), + "MarketplaceV2#_requireERC721: INVALID_ERC721_IMPLEMENTATION" + ); + } +} diff --git a/contracts/mocks/ERC20Test.sol b/contracts/mocks/ERC20Test.sol index ea633a6..0a4c1b5 100644 --- a/contracts/mocks/ERC20Test.sol +++ b/contracts/mocks/ERC20Test.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + pragma solidity >0.4.23; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/ERC721Test.sol b/contracts/mocks/ERC721Test.sol index 8808e75..efef84a 100644 --- a/contracts/mocks/ERC721Test.sol +++ b/contracts/mocks/ERC721Test.sol @@ -1,4 +1,6 @@ -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; @@ -15,8 +17,4 @@ contract ERC721Test is ERC721 { function burn(uint256 _tokenId) public { super. _burn(_tokenId); } - - function setTokenURI(uint256 _tokenId, string memory _uri) public { - super._setTokenURI(_tokenId, _uri); - } } diff --git a/contracts/mocks/ERC721TestCollection.sol b/contracts/mocks/ERC721TestCollection.sol new file mode 100644 index 0000000..3ff73ee --- /dev/null +++ b/contracts/mocks/ERC721TestCollection.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + + +contract ERC721TestCollection is ERC721 { + address public creator; + address public beneficiary; + + constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) { + } + + + function mint(address _to, uint256 _tokenId) public { + super._mint(_to, _tokenId); + } + + function burn(uint256 _tokenId) public { + super. _burn(_tokenId); + } + + function setCreator(address _creator) public { + creator = _creator; + } + + function setBeneficiary(address _beneficiary) public { + beneficiary = _beneficiary; + } + + function decodeTokenId(uint256 _tokenId) external pure returns (uint256, uint256) { + return (_tokenId, _tokenId); + } + + function items(uint256 _itemId) public view returns (string memory, uint256, uint256, uint256, address, string memory, string memory) { + if (_itemId > 0) { + return ("", 0, 0, 0, beneficiary, "", ""); + } else { + revert(); + } + } + + +} diff --git a/contracts/mocks/MarketplaceTest.sol b/contracts/mocks/MarketplaceTest.sol index f9df06a..387a589 100644 --- a/contracts/mocks/MarketplaceTest.sol +++ b/contracts/mocks/MarketplaceTest.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + pragma solidity >0.4.23; import "../../contracts/marketplace/Marketplace.sol"; diff --git a/contracts/mocks/VerifiableERC721Test.sol b/contracts/mocks/VerifiableERC721Test.sol index 0fec80a..5275051 100644 --- a/contracts/mocks/VerifiableERC721Test.sol +++ b/contracts/mocks/VerifiableERC721Test.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + pragma solidity >0.4.23; import "./ERC721Test.sol"; @@ -10,7 +12,7 @@ contract VerifiableERC721Test is ERC721Test { return getFingerprint(assetId) == _bytesToBytes32(fingerprint); } - function getFingerprint(uint256 assetId) public pure returns (bytes32) { + function getFingerprint(uint256 /*assetId*/) public pure returns (bytes32) { return bytes32(uint256(0x1234)); } diff --git a/full/Marketplace.sol b/full/Marketplace.sol index 7a27ff6..d82b2dc 100644 --- a/full/Marketplace.sol +++ b/full/Marketplace.sol @@ -1,23 +1,20 @@ -// Sources flattened with hardhat v2.0.10 https://hardhat.org +// Sources flattened with hardhat v2.3.0 https://hardhat.org -// File @openzeppelin/contracts/math/SafeMath.sol@v3.4.0 +// File @openzeppelin/contracts/utils/math/SafeMath.sol@v4.3.2 // SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.8.0; +pragma solidity ^0.8.0; + +// CAUTION +// This version of SafeMath should only be used with Solidity 0.8 or later, +// because it relies on the compiler's built in overflow checks. /** - * @dev Wrappers over Solidity's arithmetic operations with added overflow - * checks. - * - * Arithmetic operations in Solidity wrap on overflow. This can easily result - * in bugs, because programmers usually assume that an overflow raises an - * error, which is the standard behavior in high level programming languages. - * `SafeMath` restores this intuition by reverting the transaction when an - * operation overflows. + * @dev Wrappers over Solidity's arithmetic operations. * - * Using this library instead of the unchecked operations eliminates an entire - * class of bugs, so it's recommended to use it always. + * NOTE: `SafeMath` is no longer needed starting with Solidity 0.8. The compiler + * now has built in overflow checking. */ library SafeMath { /** @@ -26,9 +23,11 @@ library SafeMath { * _Available since v3.4._ */ function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { - uint256 c = a + b; - if (c < a) return (false, 0); - return (true, c); + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } } /** @@ -37,8 +36,10 @@ library SafeMath { * _Available since v3.4._ */ function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { - if (b > a) return (false, 0); - return (true, a - b); + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } } /** @@ -47,13 +48,15 @@ library SafeMath { * _Available since v3.4._ */ function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) return (true, 0); - uint256 c = a * b; - if (c / a != b) return (false, 0); - return (true, c); + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } } /** @@ -62,8 +65,10 @@ library SafeMath { * _Available since v3.4._ */ function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { - if (b == 0) return (false, 0); - return (true, a / b); + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } } /** @@ -72,8 +77,10 @@ library SafeMath { * _Available since v3.4._ */ function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { - if (b == 0) return (false, 0); - return (true, a % b); + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } } /** @@ -87,9 +94,7 @@ library SafeMath { * - Addition cannot overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, "SafeMath: addition overflow"); - return c; + return a + b; } /** @@ -103,7 +108,6 @@ library SafeMath { * - Subtraction cannot overflow. */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { - require(b <= a, "SafeMath: subtraction overflow"); return a - b; } @@ -118,26 +122,20 @@ library SafeMath { * - Multiplication cannot overflow. */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { - if (a == 0) return 0; - uint256 c = a * b; - require(c / a == b, "SafeMath: multiplication overflow"); - return c; + return a * b; } /** * @dev Returns the integer division of two unsigned integers, reverting on * division by zero. The result is rounded towards zero. * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). + * Counterpart to Solidity's `/` operator. * * Requirements: * * - The divisor cannot be zero. */ function div(uint256 a, uint256 b) internal pure returns (uint256) { - require(b > 0, "SafeMath: division by zero"); return a / b; } @@ -154,7 +152,6 @@ library SafeMath { * - The divisor cannot be zero. */ function mod(uint256 a, uint256 b) internal pure returns (uint256) { - require(b > 0, "SafeMath: modulo by zero"); return a % b; } @@ -171,18 +168,21 @@ library SafeMath { * * - Subtraction cannot overflow. */ - function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b <= a, errorMessage); - return a - b; + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b <= a, errorMessage); + return a - b; + } } /** * @dev Returns the integer division of two unsigned integers, reverting with custom message on * division by zero. The result is rounded towards zero. * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryDiv}. - * * Counterpart to Solidity's `/` operator. Note: this function uses a * `revert` opcode (which leaves remaining gas untouched) while Solidity * uses an invalid opcode to revert (consuming all remaining gas). @@ -191,9 +191,15 @@ library SafeMath { * * - The divisor cannot be zero. */ - function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b > 0, errorMessage); - return a / b; + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a / b; + } } /** @@ -211,17 +217,24 @@ library SafeMath { * * - The divisor cannot be zero. */ - function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - require(b > 0, errorMessage); - return a % b; + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a % b; + } } } -// File @openzeppelin/contracts/utils/Address.sol@v3.4.0 +// File @openzeppelin/contracts/utils/Address.sol@v4.3.2 +// SPDX-License-Identifier: MIT -pragma solidity >=0.6.2 <0.8.0; +pragma solidity ^0.8.0; /** * @dev Collection of functions related to the address type @@ -250,8 +263,9 @@ library Address { // constructor execution. uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { size := extcodesize(account) } + assembly { + size := extcodesize(account) + } return size > 0; } @@ -274,14 +288,13 @@ library Address { function sendValue(address payable recipient, uint256 amount) internal { require(address(this).balance >= amount, "Address: insufficient balance"); - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); + (bool success, ) = recipient.call{value: amount}(""); require(success, "Address: unable to send value, recipient may have reverted"); } /** * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this + * plain `call` is an unsafe replacement for a function call: use this * function instead. * * If `target` reverts with a revert reason, it is bubbled up by this @@ -298,7 +311,7 @@ library Address { * _Available since v3.1._ */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); + return functionCall(target, data, "Address: low-level call failed"); } /** @@ -307,7 +320,11 @@ library Address { * * _Available since v3.1._ */ - function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { return functionCallWithValue(target, data, 0, errorMessage); } @@ -322,7 +339,11 @@ library Address { * * _Available since v3.1._ */ - function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); } @@ -332,13 +353,17 @@ library Address { * * _Available since v3.1._ */ - function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { require(address(this).balance >= value, "Address: insufficient balance for call"); require(isContract(target), "Address: call to non-contract"); - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); } /** @@ -357,12 +382,15 @@ library Address { * * _Available since v3.3._ */ - function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { require(isContract(target), "Address: static call to non-contract"); - // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); + return verifyCallResult(success, returndata, errorMessage); } /** @@ -381,15 +409,28 @@ library Address { * * _Available since v3.4._ */ - function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { require(isContract(target), "Address: delegate call to non-contract"); - // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); + return verifyCallResult(success, returndata, errorMessage); } - function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) { + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { if (success) { return returndata; } else { @@ -397,7 +438,6 @@ library Address { if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly - // solhint-disable-next-line no-inline-assembly assembly { let returndata_size := mload(returndata) revert(add(32, returndata), returndata_size) @@ -412,7 +452,9 @@ library Address { // File contracts/marketplace/MarketplaceStorage.sol -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; /** @@ -463,8 +505,6 @@ contract MarketplaceStorage { uint256 public ownerCutPerMillion; uint256 public publicationFeeInWei; - address public legacyNFTAddress; - bytes4 public constant InterfaceId_ValidateFingerprint = bytes4( keccak256("verifyFingerprint(uint256,bytes)") ); @@ -497,34 +537,14 @@ contract MarketplaceStorage { event ChangedPublicationFee(uint256 publicationFee); event ChangedOwnerCutPerMillion(uint256 ownerCutPerMillion); - event ChangeLegacyNFTAddress(address indexed legacyNFTAddress); - - // [LEGACY] Auction events - event AuctionCreated( - bytes32 id, - uint256 indexed assetId, - address indexed seller, - uint256 priceInWei, - uint256 expiresAt - ); - event AuctionSuccessful( - bytes32 id, - uint256 indexed assetId, - address indexed seller, - uint256 totalPrice, - address indexed winner - ); - event AuctionCancelled( - bytes32 id, - uint256 indexed assetId, - address indexed seller - ); } // File contracts/commons/ContextMixin.sol -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; contract ContextMixin { @@ -553,8 +573,9 @@ contract ContextMixin { // File contracts/commons/Ownable.sol +// SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.8.0; +pragma solidity >=0.6.0; /** * @dev Contract module which provides a basic access control mechanism, where @@ -576,7 +597,7 @@ abstract contract Ownable is ContextMixin { /** * @dev Initializes the contract setting the deployer as the initial owner. */ - constructor () internal { + constructor () { address msgSender = _msgSender(); _owner = msgSender; emit OwnershipTransferred(address(0), msgSender); @@ -623,8 +644,9 @@ abstract contract Ownable is ContextMixin { // File contracts/commons/Pausable.sol +// SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.8.0; +pragma solidity >=0.6.0; /** * @dev Contract module which allows children to implement an emergency stop @@ -651,7 +673,7 @@ abstract contract Pausable is ContextMixin { /** * @dev Initializes the contract in unpaused state. */ - constructor () internal { + constructor () { _paused = false; } @@ -714,7 +736,9 @@ abstract contract Pausable is ContextMixin { // File contracts/commons/EIP712Base.sol -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; contract EIP712Base { @@ -782,7 +806,9 @@ contract EIP712Base { // File contracts/commons/NativeMetaTransaction.sol -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; contract NativeMetaTransaction is EIP712Base { bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256( @@ -886,7 +912,9 @@ contract NativeMetaTransaction is EIP712Base { // File contracts/marketplace/Marketplace.sol -pragma solidity ^0.7.6; +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; @@ -900,26 +928,25 @@ contract Marketplace is Ownable, Pausable, MarketplaceStorage, NativeMetaTransac /** * @dev Initialize this contract. Acts as a constructor * @param _acceptedToken - Address of the ERC20 accepted for this marketplace - * @param _legacyNFTAddress - Address of the NFT address used for legacy methods that don't have nftAddress as parameter + * @param _ownerCutPerMillion - owner cut per million + */ constructor ( address _acceptedToken, - address _legacyNFTAddress, + uint256 _ownerCutPerMillion, address _owner - ) - public - { + ) { // EIP712 init _initializeEIP712('Decentraland Marketplace', '1'); + // Fee init + setOwnerCutPerMillion(_ownerCutPerMillion); + require(_owner != address(0), "Invalid owner"); transferOwnership(_owner); require(_acceptedToken.isContract(), "The accepted token address must be a deployed contract"); acceptedToken = ERC20Interface(_acceptedToken); - - _requireERC721(_legacyNFTAddress); - legacyNFTAddress = _legacyNFTAddress; } @@ -937,24 +964,13 @@ contract Marketplace is Ownable, Pausable, MarketplaceStorage, NativeMetaTransac * charged to the seller on a successful sale * @param _ownerCutPerMillion - Share amount, from 0 to 999,999 */ - function setOwnerCutPerMillion(uint256 _ownerCutPerMillion) external onlyOwner { + function setOwnerCutPerMillion(uint256 _ownerCutPerMillion) public onlyOwner { require(_ownerCutPerMillion < 1000000, "The owner cut should be between 0 and 999,999"); ownerCutPerMillion = _ownerCutPerMillion; emit ChangedOwnerCutPerMillion(ownerCutPerMillion); } - /** - * @dev Sets the legacy NFT address to be used - * @param _legacyNFTAddress - Address of the NFT address used for legacy methods that don't have nftAddress as parameter - */ - function setLegacyNFTAddress(address _legacyNFTAddress) external onlyOwner { - _requireERC721(_legacyNFTAddress); - - legacyNFTAddress = _legacyNFTAddress; - emit ChangeLegacyNFTAddress(legacyNFTAddress); - } - /** * @dev Creates a new order * @param nftAddress - Non fungible registry address @@ -979,37 +995,6 @@ contract Marketplace is Ownable, Pausable, MarketplaceStorage, NativeMetaTransac ); } - /** - * @dev [LEGACY] Creates a new order - * @param assetId - ID of the published NFT - * @param priceInWei - Price in Wei for the supported coin - * @param expiresAt - Duration of the order (in hours) - */ - function createOrder( - uint256 assetId, - uint256 priceInWei, - uint256 expiresAt - ) - public - whenNotPaused - { - _createOrder( - legacyNFTAddress, - assetId, - priceInWei, - expiresAt - ); - - Order memory order = orderByAssetId[legacyNFTAddress][assetId]; - emit AuctionCreated( - order.id, - assetId, - order.seller, - order.price, - order.expiresAt - ); - } - /** * @dev Cancel an already published order * can only be canceled by seller or the contract owner @@ -1020,21 +1005,6 @@ contract Marketplace is Ownable, Pausable, MarketplaceStorage, NativeMetaTransac _cancelOrder(nftAddress, assetId); } - /** - * @dev [LEGACY] Cancel an already published order - * can only be canceled by seller or the contract owner - * @param assetId - ID of the published NFT - */ - function cancelOrder(uint256 assetId) public whenNotPaused { - Order memory order = _cancelOrder(legacyNFTAddress, assetId); - - emit AuctionCancelled( - order.id, - assetId, - order.seller - ); - } - /** * @dev Executes the sale for a published NFT and checks for the asset fingerprint * @param nftAddress - Address of the NFT registry @@ -1081,51 +1051,6 @@ contract Marketplace is Ownable, Pausable, MarketplaceStorage, NativeMetaTransac ); } - /** - * @dev [LEGACY] Executes the sale for a published NFT - * @param assetId - ID of the published NFT - * @param price - Order price - */ - function executeOrder( - uint256 assetId, - uint256 price - ) - public - whenNotPaused - { - Order memory order = _executeOrder( - legacyNFTAddress, - assetId, - price, - "" - ); - - emit AuctionSuccessful( - order.id, - assetId, - order.seller, - price, - _msgSender() - ); - } - - /** - * @dev [LEGACY] Gets an order using the legacy NFT address. - * @dev It's equivalent to orderByAssetId[legacyNFTAddress][assetId] but returns same structure as the old Auction - * @param assetId - ID of the published NFT - */ - function auctionByAssetId( - uint256 assetId - ) - public - view - returns - (bytes32, address, uint256, uint256) - { - Order memory order = orderByAssetId[legacyNFTAddress][assetId]; - return (order.id, order.seller, order.price, order.expiresAt); - } - /** * @dev Creates a new order * @param nftAddress - Non fungible registry address diff --git a/full/MarketplaceV2.sol b/full/MarketplaceV2.sol new file mode 100644 index 0000000..66e226b --- /dev/null +++ b/full/MarketplaceV2.sol @@ -0,0 +1,1344 @@ +// Sources flattened with hardhat v2.3.0 https://hardhat.org + +// File @openzeppelin/contracts/utils/Address.sol@v4.3.2 + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + + +// File @openzeppelin/contracts/token/ERC20/IERC20.sol@v4.3.2 + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} + + +// File contracts/commons/ContextMixin.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + + +contract ContextMixin { + function _msgSender() + internal + view + returns (address sender) + { + if (msg.sender == address(this)) { + bytes memory array = msg.data; + uint256 index = msg.data.length; + assembly { + // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. + sender := and( + mload(add(array, index)), + 0xffffffffffffffffffffffffffffffffffffffff + ) + } + } else { + sender = msg.sender; + } + return sender; + } +} + + +// File contracts/commons/Ownable.sol + +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is ContextMixin { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor () { + address msgSender = _msgSender(); + _owner = msgSender; + emit OwnershipTransferred(address(0), msgSender); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + emit OwnershipTransferred(_owner, address(0)); + _owner = address(0); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + emit OwnershipTransferred(_owner, newOwner); + _owner = newOwner; + } +} + + +// File contracts/commons/Pausable.sol + +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0; + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is ContextMixin { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor () { + _paused = false; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + require(!paused(), "Pausable: paused"); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + require(paused(), "Pausable: not paused"); + _; + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + + +// File contracts/commons/EIP712Base.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + + +contract EIP712Base { + struct EIP712Domain { + string name; + string version; + address verifyingContract; + bytes32 salt; + } + + bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256( + bytes( + "EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)" + ) + ); + bytes32 public domainSeparator; + + // supposed to be called once while initializing. + // one of the contractsa that inherits this contract follows proxy pattern + // so it is not possible to do this in a constructor + function _initializeEIP712( + string memory name, + string memory version + ) + internal + { + domainSeparator = keccak256( + abi.encode( + EIP712_DOMAIN_TYPEHASH, + keccak256(bytes(name)), + keccak256(bytes(version)), + address(this), + bytes32(getChainId()) + ) + ); + } + + function getChainId() public view returns (uint256) { + uint256 id; + assembly { + id := chainid() + } + return id; + } + + /** + * Accept message hash and returns hash message in EIP712 compatible form + * So that it can be used to recover signer from signature signed using EIP712 formatted data + * https://eips.ethereum.org/EIPS/eip-712 + * "\\x19" makes the encoding deterministic + * "\\x01" is the version byte to make it compatible to EIP-191 + */ + function toTypedMessageHash(bytes32 messageHash) + internal + view + returns (bytes32) + { + return + keccak256( + abi.encodePacked("\x19\x01", domainSeparator, messageHash) + ); + } +} + + +// File contracts/commons/NativeMetaTransaction.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +contract NativeMetaTransaction is EIP712Base { + bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256( + bytes( + "MetaTransaction(uint256 nonce,address from,bytes functionSignature)" + ) + ); + event MetaTransactionExecuted( + address userAddress, + address relayerAddress, + bytes functionSignature + ); + mapping(address => uint256) nonces; + + /* + * Meta transaction structure. + * No point of including value field here as if user is doing value transfer then he has the funds to pay for gas + * He should call the desired function directly in that case. + */ + struct MetaTransaction { + uint256 nonce; + address from; + bytes functionSignature; + } + + function executeMetaTransaction( + address userAddress, + bytes memory functionSignature, + bytes32 sigR, + bytes32 sigS, + uint8 sigV + ) public payable returns (bytes memory) { + MetaTransaction memory metaTx = MetaTransaction({ + nonce: nonces[userAddress], + from: userAddress, + functionSignature: functionSignature + }); + + require( + verify(userAddress, metaTx, sigR, sigS, sigV), + "NMT#executeMetaTransaction: SIGNER_AND_SIGNATURE_DO_NOT_MATCH" + ); + + // increase nonce for user (to avoid re-use) + nonces[userAddress] = nonces[userAddress] + 1; + + emit MetaTransactionExecuted( + userAddress, + msg.sender, + functionSignature + ); + + // Append userAddress and relayer address at the end to extract it from calling context + (bool success, bytes memory returnData) = address(this).call( + abi.encodePacked(functionSignature, userAddress) + ); + require(success, "NMT#executeMetaTransaction: CALL_FAILED"); + + return returnData; + } + + function hashMetaTransaction(MetaTransaction memory metaTx) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encode( + META_TRANSACTION_TYPEHASH, + metaTx.nonce, + metaTx.from, + keccak256(metaTx.functionSignature) + ) + ); + } + + function getNonce(address user) public view returns (uint256 nonce) { + nonce = nonces[user]; + } + + function verify( + address signer, + MetaTransaction memory metaTx, + bytes32 sigR, + bytes32 sigS, + uint8 sigV + ) internal view returns (bool) { + require(signer != address(0), "NMT#verify: INVALID_SIGNER"); + return + signer == + ecrecover( + toTypedMessageHash(hashMetaTransaction(metaTx)), + sigV, + sigR, + sigS + ); + } +} + + +// File @openzeppelin/contracts/utils/introspection/IERC165.sol@v4.3.2 + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + + +// File @openzeppelin/contracts/token/ERC721/IERC721.sol@v4.3.2 + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 is IERC165 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; +} + + +// File contracts/interfaces/IERC721Verifiable.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +interface IERC721Verifiable is IERC721 { + function verifyFingerprint(uint256, bytes memory) external view returns (bool); +} + + +// File contracts/interfaces/IRoyaltiesManager.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +interface IRoyaltiesManager { + function getRoyaltiesReceiver(address _contractAddress, uint256 _tokenId) external view returns (address); +} + + +// File contracts/marketplace/MarketplaceV2.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + + + + + + + +contract MarketplaceV2 is Ownable, Pausable, NativeMetaTransaction { + using Address for address; + + IERC20 public acceptedToken; + + struct Order { + // Order ID + bytes32 id; + // Owner of the NFT + address seller; + // NFT registry address + address nftAddress; + // Price (in wei) for the published item + uint256 price; + // Time when this sale ends + uint256 expiresAt; + } + + // From ERC721 registry assetId to Order (to avoid asset collision) + mapping (address => mapping(uint256 => Order)) public orderByAssetId; + + address public feesCollector; + IRoyaltiesManager public royaltiesManager; + + uint256 public feesCollectorCutPerMillion; + uint256 public royaltiesCutPerMillion; + uint256 public publicationFeeInWei; + + + bytes4 public constant InterfaceId_ValidateFingerprint = bytes4( + keccak256("verifyFingerprint(uint256,bytes)") + ); + + bytes4 public constant ERC721_Interface = bytes4(0x80ac58cd); + + // EVENTS + event OrderCreated( + bytes32 id, + uint256 indexed assetId, + address indexed seller, + address nftAddress, + uint256 priceInWei, + uint256 expiresAt + ); + event OrderSuccessful( + bytes32 id, + uint256 indexed assetId, + address indexed seller, + address nftAddress, + uint256 totalPrice, + address indexed buyer + ); + event OrderCancelled( + bytes32 id, + uint256 indexed assetId, + address indexed seller, + address nftAddress + ); + + event ChangedPublicationFee(uint256 publicationFee); + event ChangedFeesCollectorCutPerMillion(uint256 feesCollectorCutPerMillion); + event ChangedRoyaltiesCutPerMillion(uint256 royaltiesCutPerMillion); + event FeesCollectorSet(address indexed oldFeesCollector, address indexed newFeesCollector); + event RoyaltiesManagerSet(IRoyaltiesManager indexed oldRoyaltiesManager, IRoyaltiesManager indexed newRoyaltiesManager); + + + /** + * @dev Initialize this contract. Acts as a constructor + * @param _owner - owner + * @param _feesCollector - fees collector + * @param _acceptedToken - Address of the ERC20 accepted for this marketplace + * @param _royaltiesManager - Royalties manager contract + * @param _feesCollectorCutPerMillion - fees collector cut per million + * @param _royaltiesCutPerMillion - royalties cut per million + */ + constructor ( + address _owner, + address _feesCollector, + address _acceptedToken, + IRoyaltiesManager _royaltiesManager, + uint256 _feesCollectorCutPerMillion, + uint256 _royaltiesCutPerMillion + ) { + // EIP712 init + _initializeEIP712('Decentraland Marketplace', '2'); + + // Address init + setFeesCollector(_feesCollector); + setRoyaltiesManager(_royaltiesManager); + + // Fee init + setFeesCollectorCutPerMillion(_feesCollectorCutPerMillion); + setRoyaltiesCutPerMillion(_royaltiesCutPerMillion); + + require(_owner != address(0), "MarketplaceV2#constructor: INVALID_OWNER"); + transferOwnership(_owner); + + require(_acceptedToken.isContract(), "MarketplaceV2#constructor: INVALID_ACCEPTED_TOKEN"); + acceptedToken = IERC20(_acceptedToken); + } + + + /** + * @dev Sets the publication fee that's charged to users to publish items + * @param _publicationFee - Fee amount in wei this contract charges to publish an item + */ + function setPublicationFee(uint256 _publicationFee) external onlyOwner { + publicationFeeInWei = _publicationFee; + emit ChangedPublicationFee(publicationFeeInWei); + } + + /** + * @dev Sets the share cut for the fees collector of the contract that's + * charged to the seller on a successful sale + * @param _feesCollectorCutPerMillion - fees for the collector + */ + function setFeesCollectorCutPerMillion(uint256 _feesCollectorCutPerMillion) public onlyOwner { + feesCollectorCutPerMillion = _feesCollectorCutPerMillion; + + require( + feesCollectorCutPerMillion + royaltiesCutPerMillion < 1000000, + "MarketplaceV2#setFeesCollectorCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999" + ); + + emit ChangedFeesCollectorCutPerMillion(feesCollectorCutPerMillion); + } + + /** + * @dev Sets the share cut for the royalties that's + * charged to the seller on a successful sale + * @param _royaltiesCutPerMillion - fees for royalties + */ + function setRoyaltiesCutPerMillion(uint256 _royaltiesCutPerMillion) public onlyOwner { + royaltiesCutPerMillion = _royaltiesCutPerMillion; + + require( + feesCollectorCutPerMillion + royaltiesCutPerMillion < 1000000, + "MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999" + ); + + emit ChangedRoyaltiesCutPerMillion(royaltiesCutPerMillion); + } + + /** + * @notice Set the fees collector + * @param _newFeesCollector - fees collector + */ + function setFeesCollector(address _newFeesCollector) onlyOwner public { + require(_newFeesCollector != address(0), "MarketplaceV2#setFeesCollector: INVALID_FEES_COLLECTOR"); + + emit FeesCollectorSet(feesCollector, _newFeesCollector); + feesCollector = _newFeesCollector; + } + + /** + * @notice Set the royalties manager + * @param _newRoyaltiesManager - royalties manager + */ + function setRoyaltiesManager(IRoyaltiesManager _newRoyaltiesManager) onlyOwner public { + require(address(_newRoyaltiesManager).isContract(), "MarketplaceV2#setRoyaltiesManager: INVALID_ROYALTIES_MANAGER"); + + + emit RoyaltiesManagerSet(royaltiesManager, _newRoyaltiesManager); + royaltiesManager = _newRoyaltiesManager; + } + + + /** + * @dev Creates a new order + * @param nftAddress - Non fungible registry address + * @param assetId - ID of the published NFT + * @param priceInWei - Price in Wei for the supported coin + * @param expiresAt - Duration of the order (in hours) + */ + function createOrder( + address nftAddress, + uint256 assetId, + uint256 priceInWei, + uint256 expiresAt + ) + public + whenNotPaused + { + _createOrder( + nftAddress, + assetId, + priceInWei, + expiresAt + ); + } + + /** + * @dev Cancel an already published order + * can only be canceled by seller or the contract owner + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + */ + function cancelOrder(address nftAddress, uint256 assetId) public whenNotPaused { + _cancelOrder(nftAddress, assetId); + } + + /** + * @dev Executes the sale for a published NFT and checks for the asset fingerprint + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + * @param price - Order price + * @param fingerprint - Verification info for the asset + */ + function safeExecuteOrder( + address nftAddress, + uint256 assetId, + uint256 price, + bytes memory fingerprint + ) + public + whenNotPaused + { + _executeOrder( + nftAddress, + assetId, + price, + fingerprint + ); + } + + /** + * @dev Executes the sale for a published NFT + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + * @param price - Order price + */ + function executeOrder( + address nftAddress, + uint256 assetId, + uint256 price + ) + public + whenNotPaused + { + _executeOrder( + nftAddress, + assetId, + price, + "" + ); + } + + /** + * @dev Creates a new order + * @param nftAddress - Non fungible registry address + * @param assetId - ID of the published NFT + * @param priceInWei - Price in Wei for the supported coin + * @param expiresAt - Duration of the order (in hours) + */ + function _createOrder( + address nftAddress, + uint256 assetId, + uint256 priceInWei, + uint256 expiresAt + ) + internal + { + _requireERC721(nftAddress); + + address sender = _msgSender(); + + IERC721Verifiable nftRegistry = IERC721Verifiable(nftAddress); + address assetOwner = nftRegistry.ownerOf(assetId); + + require(sender == assetOwner, "MarketplaceV2#_createOrder: NOT_ASSET_OWNER"); + require( + nftRegistry.getApproved(assetId) == address(this) || nftRegistry.isApprovedForAll(assetOwner, address(this)), + "The contract is not authorized to manage the asset" + ); + require(priceInWei > 0, "Price should be bigger than 0"); + require(expiresAt > block.timestamp + 1 minutes, "MarketplaceV2#_createOrder: INVALID_EXPIRES_AT"); + + bytes32 orderId = keccak256( + abi.encodePacked( + block.timestamp, + assetOwner, + assetId, + nftAddress, + priceInWei + ) + ); + + orderByAssetId[nftAddress][assetId] = Order({ + id: orderId, + seller: assetOwner, + nftAddress: nftAddress, + price: priceInWei, + expiresAt: expiresAt + }); + + // Check if there's a publication fee and + // transfer the amount to marketplace owner + if (publicationFeeInWei > 0) { + require( + acceptedToken.transferFrom(sender, feesCollector, publicationFeeInWei), + "MarketplaceV2#_createOrder: TRANSFER_FAILED" + ); + } + + emit OrderCreated( + orderId, + assetId, + assetOwner, + nftAddress, + priceInWei, + expiresAt + ); + } + + /** + * @dev Cancel an already published order + * can only be canceled by seller or the contract owner + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + */ + function _cancelOrder(address nftAddress, uint256 assetId) internal returns (Order memory) { + address sender = _msgSender(); + Order memory order = orderByAssetId[nftAddress][assetId]; + + require(order.id != 0, "MarketplaceV2#_cancelOrder: INVALID_ORDER"); + require(order.seller == sender || sender == owner(), "MarketplaceV2#_cancelOrder: UNAUTHORIZED_USER"); + + bytes32 orderId = order.id; + address orderSeller = order.seller; + address orderNftAddress = order.nftAddress; + delete orderByAssetId[nftAddress][assetId]; + + emit OrderCancelled( + orderId, + assetId, + orderSeller, + orderNftAddress + ); + + return order; + } + + /** + * @dev Executes the sale for a published NFT + * @param nftAddress - Address of the NFT registry + * @param assetId - ID of the published NFT + * @param price - Order price + * @param fingerprint - Verification info for the asset + */ + function _executeOrder( + address nftAddress, + uint256 assetId, + uint256 price, + bytes memory fingerprint + ) + internal returns (Order memory) + { + _requireERC721(nftAddress); + + address sender = _msgSender(); + + IERC721Verifiable nftRegistry = IERC721Verifiable(nftAddress); + + if (nftRegistry.supportsInterface(InterfaceId_ValidateFingerprint)) { + require( + nftRegistry.verifyFingerprint(assetId, fingerprint), + "MarketplaceV2#_executeOrder: INVALID_FINGERPRINT" + ); + } + Order memory order = orderByAssetId[nftAddress][assetId]; + + require(order.id != 0, "MarketplaceV2#_executeOrder: ASSET_NOT_FOR_SALE"); + + require(order.seller != address(0), "MarketplaceV2#_executeOrder: INVALID_SELLER"); + require(order.seller != sender, "MarketplaceV2#_executeOrder: SENDER_IS_SELLER"); + require(order.price == price, "MarketplaceV2#_executeOrder: PRICE_MISMATCH"); + require(block.timestamp < order.expiresAt, "MarketplaceV2#_executeOrder: ORDER_EXPIRED"); + require(order.seller == nftRegistry.ownerOf(assetId), "MarketplaceV2#_executeOrder: SELLER_NOT_OWNER"); + + + delete orderByAssetId[nftAddress][assetId]; + + uint256 feesCollectorShareAmount; + uint256 royaltiesShareAmount; + address royaltiesReceiver; + + // Royalties share + if (royaltiesCutPerMillion > 0) { + royaltiesShareAmount = (price * royaltiesCutPerMillion) / 1000000; + + (bool success, bytes memory res) = address(royaltiesManager).staticcall( + abi.encodeWithSelector( + royaltiesManager.getRoyaltiesReceiver.selector, + address(nftRegistry), + assetId + ) + ); + + if (success) { + (royaltiesReceiver) = abi.decode(res, (address)); + if (royaltiesReceiver != address(0)) { + require( + acceptedToken.transferFrom(sender, royaltiesReceiver, royaltiesShareAmount), + "MarketplaceV2#_executeOrder: TRANSFER_FEES_TO_ROYALTIES_RECEIVER_FAILED" + ); + } + } + } + + // Fees collector share + { + feesCollectorShareAmount = (price * feesCollectorCutPerMillion) / 1000000; + uint256 totalFeeCollectorShareAmount = feesCollectorShareAmount; + + if (royaltiesShareAmount > 0 && royaltiesReceiver == address(0)) { + totalFeeCollectorShareAmount += royaltiesShareAmount; + } + + if (totalFeeCollectorShareAmount > 0) { + require( + acceptedToken.transferFrom(sender, feesCollector, totalFeeCollectorShareAmount), + "MarketplaceV2#_executeOrder: TRANSFER_FEES_TO_FEES_COLLECTOR_FAILED" + ); + } + } + + // Transfer sale amount to seller + require( + acceptedToken.transferFrom(sender, order.seller, price - royaltiesShareAmount - feesCollectorShareAmount), + "MarketplaceV2#_executeOrder: TRANSFER_AMOUNT_TO_SELLER_FAILED" + ); + + // Transfer asset owner + nftRegistry.safeTransferFrom( + order.seller, + sender, + assetId + ); + + emit OrderSuccessful( + order.id, + assetId, + order.seller, + nftAddress, + price, + sender + ); + + return order; + } + + function _requireERC721(address nftAddress) internal view { + require(nftAddress.isContract(), "MarketplaceV2#_requireERC721: INVALID_NFT_ADDRESS"); + + IERC721 nftRegistry = IERC721(nftAddress); + require( + nftRegistry.supportsInterface(ERC721_Interface), + "MarketplaceV2#_requireERC721: INVALID_ERC721_IMPLEMENTATION" + ); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 4f52ae8..8db5186 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -23,6 +23,15 @@ module.exports = { }, }, }, + { + version: '0.8.10', + settings: { + optimizer: { + enabled: true, + runs: 1, + }, + }, + }, ], }, networks: { @@ -45,4 +54,7 @@ module.exports = { gasPrice: 21, showTimeSpent: true, }, + etherscan: { + apiKey: process.env.ETHERSCAN_API_KEY + }, } diff --git a/package-lock.json b/package-lock.json index 307ede6..6572fe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -799,11 +799,12 @@ "integrity": "sha512-6quxWe8wwS4X5v3Au8q1jOvXYEPkS1Fh+cME5u6AwNdnI4uERvPlVjlgRWzpnb+Rrt1l/cEqiNRH9GlsBMSDQg==" }, "@nomiclabs/hardhat-etherscan": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.2.tgz", - "integrity": "sha512-SExzaBuHlnmHw0HKkElHITzdvhUQmlIRc2tlaywzgvPbh7WoI24nYqZ4N0CO+JXSDgRpFycvQNA8zRaCqjuqUg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.7.tgz", + "integrity": "sha512-9nt9EXubnkd2aTMnQIqKtp80bQFhun88krfB31FN2wB0T54b8YuK0riG2d+EKq/D3t1Kb00oA7oFSFpHLIbLDQ==", + "dev": true, "requires": { - "@ethersproject/abi": "^5.0.2", + "@ethersproject/abi": "^5.1.2", "@ethersproject/address": "^5.0.2", "cbor": "^5.0.2", "debug": "^4.1.1", @@ -813,9 +814,10 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -824,6 +826,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -834,6 +837,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, "requires": { "graceful-fs": "^4.1.6" } @@ -841,17 +845,23 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -1181,9 +1191,9 @@ } }, "@openzeppelin/contracts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.1.tgz", - "integrity": "sha512-cUriqMauq1ylzP2TxePNdPqkwI7Le3Annh4K9rrpvKfSBB/bdW+Iu1ihBaTIABTAAJ85LmKL5SSPPL9ry8d1gQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.2.tgz", + "integrity": "sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg==", "dev": true }, "@semantic-release/commit-analyzer": { @@ -3923,6 +3933,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, "requires": { "bignumber.js": "^9.0.1", "nofilter": "^1.0.4" @@ -3931,7 +3942,8 @@ "bignumber.js": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", - "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", + "dev": true } } }, @@ -4607,6 +4619,13 @@ "truffle-contract": "^4.0.11", "ts-node": "^8.0.3", "typescript": "^4.1.3" + }, + "dependencies": { + "openzeppelin-solidity": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.5.1.tgz", + "integrity": "sha512-oCGtQPLOou4su76IMr4XXJavy9a8OZmAXeUZ8diOdFznlL/mlkIlYr7wajqCzH4S47nlKPS7m0+a2nilCTpVPQ==" + } } }, "decentraland-mana": { @@ -10361,7 +10380,8 @@ "nofilter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==" + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true }, "normalize-package-data": { "version": "2.5.0", @@ -14137,11 +14157,6 @@ "mimic-fn": "^2.1.0" } }, - "openzeppelin-solidity": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.5.1.tgz", - "integrity": "sha512-oCGtQPLOou4su76IMr4XXJavy9a8OZmAXeUZ8diOdFznlL/mlkIlYr7wajqCzH4S47nlKPS7m0+a2nilCTpVPQ==" - }, "original-require": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/original-require/-/original-require-1.0.1.tgz", @@ -17454,6 +17469,12 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, "traverse": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", @@ -18899,7 +18920,7 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.16.0.tgz", "integrity": "sha1-pFVBdc1GKUMDWx8dOUMvdBxrYBk=", "requires": { - "bignumber.js": "git+https://github.com/debris/bignumber.js.git#master", + "bignumber.js": "git+https://github.com/debris/bignumber.js.git#c7a38de919ed75e6fb6ba38051986e294b328df9", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xmlhttprequest": "*" @@ -18933,7 +18954,7 @@ "requires": { "underscore": "1.9.1", "web3-core-helpers": "1.2.1", - "websocket": "github:web3-js/WebSocket-Node#polyfill/globalThis" + "websocket": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400" } }, "web3-shh": { @@ -18978,6 +18999,12 @@ } } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, "websocket": { "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", @@ -19009,6 +19036,16 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 50755cc..1434746 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "Smart contracts for Decentraland Marketplace", "main": "index.js", "devDependencies": { + "@nomiclabs/hardhat-etherscan": "^2.1.7", "@nomiclabs/hardhat-truffle5": "^2.0.0", "@nomiclabs/hardhat-web3": "^2.0.0", - "@openzeppelin/contracts": "^3.4.0", + "@openzeppelin/contracts": "^4.3.2", "abi-decoder": "^1.2.0", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-polyfill": "^6.26.0", @@ -39,7 +40,6 @@ "license": "ISC", "dependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", - "@nomiclabs/hardhat-etherscan": "^2.1.2", "decentraland-contract-plugins": "^2.2.0", "ethers": "^5.0.0" }, diff --git a/scripts/DeployRoyaltiesManager.ts b/scripts/DeployRoyaltiesManager.ts new file mode 100644 index 0000000..e19242d --- /dev/null +++ b/scripts/DeployRoyaltiesManager.ts @@ -0,0 +1,36 @@ +import { ethers } from "hardhat" + + +enum NETWORKS { + 'MUMBAI' = 'MUMBAI', + 'MATIC' = 'MATIC', + 'GOERLI' = 'GOERLI', + 'LOCALHOST' = 'LOCALHOST', + 'BSC_TESTNET' = 'BSC_TESTNET', +} + +/** + * @dev Steps: + * Deploy the Royalties Manager + */ +async function main() { + const network = NETWORKS[(process.env['NETWORK'] || 'LOCALHOST') as NETWORKS] + if (!network) { + throw ('Invalid network') + } + + const a = await ethers.provider.getSigner().getAddress() + console.log(a) + + const RoyaltiesManager = await ethers.getContractFactory("RoyaltiesManager") + const royaltiesManager = await RoyaltiesManager.deploy() + + console.log('Royalties Manager:', royaltiesManager.address) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) \ No newline at end of file diff --git a/scripts/buildfull.sh b/scripts/buildfull.sh index c5f197c..885cfa3 100755 --- a/scripts/buildfull.sh +++ b/scripts/buildfull.sh @@ -1,10 +1,13 @@ #! /bin/bash MARKETPLACE=Marketplace.sol +MARKETPLACEV2=MarketplaceV2.sol + OUTPUT=full npx hardhat flatten contracts/marketplace/$MARKETPLACE > $OUTPUT/$MARKETPLACE +npx hardhat flatten contracts/marketplace/$MARKETPLACEV2 > $OUTPUT/$MARKETPLACEV2 diff --git a/scripts/deployMarketplaceV2.ts b/scripts/deployMarketplaceV2.ts new file mode 100644 index 0000000..46785ff --- /dev/null +++ b/scripts/deployMarketplaceV2.ts @@ -0,0 +1,70 @@ +import { ethers } from "hardhat" +import * as ManaConfig from 'decentraland-mana/build/contracts/MANAToken.json' + +import { + MANA_BYTECODE +} from './utils' + + +enum NETWORKS { + 'MUMBAI' = 'MUMBAI', + 'MATIC' = 'MATIC', + 'GOERLI' = 'GOERLI', + 'LOCALHOST' = 'LOCALHOST', + 'BSC_TESTNET' = 'BSC_TESTNET', +} + +enum MANA { + 'MUMBAI' = '0x882Da5967c435eA5cC6b09150d55E8304B838f45', + 'MATIC' = '0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4', + 'GOERLI' = '0xe7fDae84ACaba2A5Ba817B6E6D8A2d415DBFEdbe', + 'LOCALHOST' = '0xe7fDae84ACaba2A5Ba817B6E6D8A2d415DBFEdbe', + 'BSC_TESTNET' = '0x00cca1b48a7b41c57821492efd0e872984db5baa', +} + +const FEES_COLLECTOR_CUT_PER_MILLION = 0 +const ROYALTIES_CUT_PER_MILLION = 25000 +const ROYALTIES_MANAGER = '0x90958D4531258ca11D18396d4174a007edBc2b42' + + +/** + * @dev Steps: + * Deploy the Marketplace V2 + */ +async function main() { + const owner = process.env['OWNER'] + const feeCollector = process.env['FEE_COLLECTOR'] + + const network = NETWORKS[(process.env['NETWORK'] || 'LOCALHOST') as NETWORKS] + if (!network) { + throw ('Invalid network') + } + + // Deploy collection marketplace + let acceptedToken: string = MANA[network] + + if (network === 'LOCALHOST') { + const Mana = new ethers.ContractFactory(ManaConfig.abi, MANA_BYTECODE, ethers.provider.getSigner()) + const mana = await Mana.deploy() + acceptedToken = mana.address + } + + const Marketplace = await ethers.getContractFactory("MarketplaceV2") + const marketplace = await Marketplace.deploy( + owner, + feeCollector, + acceptedToken, + ROYALTIES_MANAGER, + FEES_COLLECTOR_CUT_PER_MILLION, + ROYALTIES_CUT_PER_MILLION + ) + + console.log('NFT Marketplace:', marketplace.address) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) \ No newline at end of file diff --git a/scripts/utils.ts b/scripts/utils.ts index 36740a2..d8bcd55 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -9,12 +9,13 @@ export const SET_EDITABLE_SELECTOR = '0x2cb0d48a' export function getDeployParams() { return { url: process.env[`RPC_URL_${process.env['NETWORK']}`] || 'https://rinkeby.infura.io/v3/', - accounts: { + accounts: process.env['MNEMONIC'] ? { mnemonic: process.env['MNEMONIC'] ? process.env['MNEMONIC'].replace(/,/g, ' ') : 'test test test test test test test test test test test junk', + initialIndex: 0, count: 10, path: `m/44'/60'/0'/0` - }, + } : [process.env['PRIV_KEY'] ? process.env['PRIV_KEY'] : '0x12345678911111111111111111111111111111111111111111111111111111'], gas: "auto", gasPrice: "auto", gasMultiplier: 1, diff --git a/scripts/verifyContract.ts b/scripts/verifyContract.ts new file mode 100644 index 0000000..652f340 --- /dev/null +++ b/scripts/verifyContract.ts @@ -0,0 +1,22 @@ +import hr from 'hardhat' + +async function main() { + await hr.run("verify:verify", { + address: '0x480a0f4e360E8964e68858Dd231c2922f1df45Ef', + constructorArguments: [ + process.env['OWNER'], + process.env['FEE_COLLECTOR'], + '0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4', + '0x90958D4531258ca11D18396d4174a007edBc2b42', + 0, + 25000 + ], + }) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) \ No newline at end of file diff --git a/test/MarketplaceV2.js b/test/MarketplaceV2.js new file mode 100644 index 0000000..bc07ebe --- /dev/null +++ b/test/MarketplaceV2.js @@ -0,0 +1,2678 @@ +const BN = web3.utils.BN +const expect = require('chai').use(require('bn-chai')(BN)).expect + +const abiDecoder = require('abi-decoder') +const { Erc721 } = require('decentraland-contract-plugins') + +const EVMRevert = 'VM Exception while processing transaction: revert' + +const RoyaltiesManager = artifacts.require('RoyaltiesManager') +const Marketplace = artifacts.require('MarketplaceV2') +const ERC20Token = artifacts.require('ERC20Test') +const ERC721Token = artifacts.require('ERC721Test') +const ERC721Collection = artifacts.require('ERC721TestCollection') +const VerfiableERC721Token = artifacts.require('VerifiableERC721Test') + +const { increaseTime, duration } = require('./helpers/increaseTime') +const { sendMetaTx } = require('./helpers/metaTx') +const { assertRevert } = require('./helpers/assertRevert') + +function checkOrderCreatedLogs( + logs, + assetId, + seller, + nftAddress, + priceInWei, + expiresAt +) { + logs.forEach((log, index) => { + if (index === 0) { + log.event.should.be.equal('OrderCreated') + log.args.nftAddress + .toLowerCase() + .should.be.equal(nftAddress.toLowerCase(), 'nftAddress') + } else { + log.event.should.be.equal('AuctionCreated') + } + log.args.assetId.should.be.eq.BN(assetId, 'assetId') + log.args.seller + .toLowerCase() + .should.be.equal(seller.toLowerCase(), 'seller') + log.args.priceInWei.should.be.eq.BN(priceInWei, 'priceInWei') + log.args.expiresAt.should.be.eq.BN(expiresAt, 'expiresAt') + }) +} + +function checkOrderCancelledLogs(logs, assetId, seller, nftAddress) { + logs.forEach((log, index) => { + if (index === 0) { + log.event.should.be.equal('OrderCancelled') + log.args.nftAddress + .toLowerCase() + .should.be.equal(nftAddress.toLowerCase(), 'nftAddress') + } else { + log.event.should.be.equal('AuctionCancelled') + } + log.args.assetId.should.be.eq.BN(assetId, 'assetId') + log.args.seller + .toLowerCase() + .should.be.equal(seller.toLowerCase(), 'seller') + }) +} + +function checkOrderSuccessfulLogs( + logs, + assetId, + seller, + nftAddress, + totalPrice, + buyer +) { + logs.forEach((log, index) => { + if (index === 0) { + log.event.should.be.equal('OrderSuccessful') + log.args.nftAddress + .toLowerCase() + .should.be.equal(nftAddress.toLowerCase(), 'nftAddress') + log.args.buyer.toLowerCase().should.be.equal(buyer.toLowerCase(), 'buyer') + } else { + log.event.should.be.equal('AuctionSuccessful') + log.args.winner + .toLowerCase() + .should.be.equal(buyer.toLowerCase(), 'buyer') + } + log.args.assetId.should.be.eq.BN(assetId, 'assetId') + log.args.seller + .toLowerCase() + .should.be.equal(seller.toLowerCase(), 'seller') + log.args.totalPrice.should.be.eq.BN(totalPrice, 'totalPrice') + }) +} + +function checkChangedPublicationFeeLog(log, publicationFee) { + log.event.should.be.equal('ChangedPublicationFee') + log.args.publicationFee.should.be.eq.BN(publicationFee, 'publicationFee') +} + +function checkChangedFeesCollectorCutPerMillionLog( + log, + feesCollectorCutPerMillion +) { + log.event.should.be.equal('ChangedFeesCollectorCutPerMillion') + log.args.feesCollectorCutPerMillion.should.be.eq.BN( + feesCollectorCutPerMillion, + 'feesCollectorCutPerMillion' + ) +} + +function checkChangedRoyaltiesCutPerMillionLog(log, royaltiesCutPerMillion) { + log.event.should.be.equal('ChangedRoyaltiesCutPerMillion') + log.args.royaltiesCutPerMillion.should.be.eq.BN( + royaltiesCutPerMillion, + 'royaltiesCutPerMillion' + ) +} + +function checkFeesCollectorSetLog(log, oldFeesCollector, newFeesCollector) { + log.event.should.be.equal('FeesCollectorSet') + log.args.oldFeesCollector.should.be.eq.BN( + oldFeesCollector, + 'oldFeesCollector' + ) + log.args.newFeesCollector.should.be.eq.BN( + newFeesCollector, + 'newFeesCollector' + ) +} + +function checkRoyaltiesManagerSetLog( + log, + oldRoyaltiesManager, + newRoyaltiesManager +) { + log.event.should.be.equal('RoyaltiesManagerSet') + log.args.oldRoyaltiesManager.should.be.eq.BN( + oldRoyaltiesManager, + 'oldRoyaltiesManager' + ) + log.args.newRoyaltiesManager.should.be.eq.BN( + newRoyaltiesManager, + 'newRoyaltiesManager' + ) +} + +async function getEndTime(minutesAhead = 15) { + const block = await web3.eth.getBlock('latest') + return block.timestamp + duration.minutes(minutesAhead) +} + +contract('Marketplace V2', function([ + _, + owner, + seller, + buyer, + otherAddress, + relayer, + feesCollector, + itemCreator, + itemBeneficiary, + anotherUser, +]) { + const itemPrice = web3.utils.toWei('1', 'ether') + const itemPrice2 = web3.utils.toWei('2', 'ether') + + const assetId = 10000 + const notLegacyAssetId = 2 + const zeroAddress = '0x0000000000000000000000000000000000000000' + const domain = 'Decentraland Marketplace' + const version = '2' + + let royaltiesManager + let market + let erc20 + let erc721 + let verifiableErc721 + let erc721Collection + + let fingerprint + let endTime + + const fromOwner = { + from: owner, + } + + const creationParams = { + ...fromOwner, + gas: 6e6, + gasPrice: 21e9, + } + + async function createOrder(...params) { + return callMethod('createOrder', 'address,uint256,uint256,uint256', params) + } + async function executeOrder(...params) { + return callMethod('executeOrder', 'address,uint256,uint256', params) + } + async function cancelOrder(...params) { + return callMethod('cancelOrder', 'address,uint256', params) + } + + // Makeshift method to support solidity overloads ( https://github.com/trufflesuite/truffle/issues/737 ): + // Truffle does not support overloads correctly. The only way to call them is using the form: + // instance.contract.method[args](params) + // but that doesn't return the same result as calling the same method with: + // instance.method(params) + // To remedy this, we had to decode the logs ourselves and mimic the structure returned in the receipt logs + async function callMethod(methodName, argTypes, params) { + const lastParam = params[params.length - 1] + + if (typeof lastParam === 'object') { + lastParam.gas = lastParam.gas || 6e6 + lastParam.gasPrice = lastParam.gasPrice || 21e9 + } else { + params.push({ gas: 6e6, gasPrice: 21e9 }) + } + + const { tx } = await market.methods[`${methodName}(${argTypes})`](...params) + const receipt = await new Promise((resolve, reject) => + web3.eth.getTransactionReceipt(tx, (err, data) => + err ? reject(err) : resolve(data) + ) + ) + + const decodedLogs = abiDecoder.decodeLogs(receipt.logs) + receipt.logs = decodedLogs + .filter((log) => !!log) + .map((log) => ({ + event: log.name, + args: log.events.reduce( + (args, arg) => ({ ...args, [arg.name]: arg.value }), + {} + ), + })) + + return receipt + } + + beforeEach(async function() { + // Create tokens + erc20 = await ERC20Token.new('Mana', 'MANA', creationParams) + erc721 = await ERC721Token.new('LAND', 'DCL', creationParams) + verifiableErc721 = await VerfiableERC721Token.new( + 'LAND', + 'DCL', + creationParams + ) + erc721Collection = await ERC721Collection.new( + 'COLLECTION', + 'COL', + creationParams + ) + + // Create a Marketplace with mocks + royaltiesManager = await RoyaltiesManager.new({ + from: owner, + }) + + // Create a Marketplace with mocks + market = await Marketplace.new( + owner, + feesCollector, + erc20.address, + royaltiesManager.address, + 0, + 0, + { + from: owner, + } + ) + + // Set holder of the asset and aproved on registry + await erc721.mint(seller, assetId) + await erc721.setApprovalForAll(market.address, true, { from: seller }) + await erc721.setApprovalForAll(market.address, true, { from: buyer }) + + await verifiableErc721.mint(seller, assetId) + await verifiableErc721.mint(seller, notLegacyAssetId) + await verifiableErc721.setApprovalForAll(market.address, true, { + from: seller, + }) + await verifiableErc721.setApprovalForAll(market.address, true, { + from: buyer, + }) + + await erc721Collection.mint(seller, 0) // return beneficiary + await erc721Collection.mint(seller, assetId) // return creator + await erc721Collection.setApprovalForAll(market.address, true, { + from: seller, + }) + await erc721Collection.setApprovalForAll(market.address, true, { + from: buyer, + }) + + // Assign balance to buyer and allow marketplace to move ERC20 + await erc20.setBalance(buyer, web3.utils.toWei('10', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10', 'ether')) + await erc20.approve(market.address, web3.utils.toWei('30', 'ether'), { + from: seller, + }) + await erc20.approve(market.address, web3.utils.toWei('30', 'ether'), { + from: buyer, + }) + + endTime = await getEndTime() + + abiDecoder.addABI(market.abi) + }) + + describe('Initialize', function() { + it('should initialize with token', async function() { + let _market = await Marketplace.new( + owner, + feesCollector, + erc20.address, + royaltiesManager.address, + 0, + 0, + { + from: owner, + } + ) + let acceptedToken = await _market.acceptedToken.call() + acceptedToken.should.be.be.equal(erc20.address) + }) + + it('should revert if owner is invalid', async function() { + await assertRevert( + Marketplace.new( + zeroAddress, + feesCollector, + erc20.address, + royaltiesManager.address, + 0, + 0, + { + from: owner, + } + ), + 'MarketplaceV2#constructor: INVALID_OWNER' + ) + }) + + it('should revert if fees collector is invalid', async function() { + await assertRevert( + Marketplace.new( + owner, + zeroAddress, + erc20.address, + royaltiesManager.address, + 0, + 0, + { + from: owner, + } + ), + 'MarketplaceV2#setFeesCollector: INVALID_FEES_COLLECTOR' + ) + }) + + it('should revert if accepted token is invalid', async function() { + await assertRevert( + Marketplace.new( + owner, + feesCollector, + zeroAddress, + royaltiesManager.address, + 0, + 0, + { + from: owner, + } + ), + 'MarketplaceV2#constructor: INVALID_ACCEPTED_TOKEN' + ) + }) + + it('should revert if royalties manager is invalid', async function() { + await assertRevert( + Marketplace.new( + owner, + feesCollector, + erc20.address, + zeroAddress, + 0, + 0, + { + from: owner, + } + ), + 'MarketplaceV2#setRoyaltiesManager: INVALID_ROYALTIES_MANAGER' + ) + }) + + it('should revert if fee is invalid', async function() { + await assertRevert( + Marketplace.new( + owner, + feesCollector, + erc20.address, + royaltiesManager.address, + 1000000, + 0, + { + from: owner, + } + ), + 'MarketplaceV2#setFeesCollectorCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + + it('should revert if royalties is invalid', async function() { + await assertRevert( + Marketplace.new( + owner, + feesCollector, + erc20.address, + royaltiesManager.address, + 0, + 1000000, + { + from: owner, + } + ), + 'MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + + it('should revert if the sum of the fees and royalties are invalid', async function() { + await assertRevert( + Marketplace.new( + owner, + feesCollector, + erc20.address, + royaltiesManager.address, + 1, + 999999, + { + from: owner, + } + ), + 'MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + + await assertRevert( + Marketplace.new( + owner, + feesCollector, + erc20.address, + royaltiesManager.address, + 500000, + 500000, + { + from: owner, + } + ), + 'MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + }) + + describe('Create', function() { + it('should create a new order', async function() { + const { logs } = await createOrder( + erc721.address, + assetId, + itemPrice, + endTime, + { from: seller } + ) + + logs.length.should.be.equal(1) + + checkOrderCreatedLogs( + logs, + assetId, + seller, + erc721.address, + itemPrice, + endTime + ) + // Check data + let s = await market.orderByAssetId.call(erc721.address, assetId) + s[1].should.be.equal(seller) + s[2].should.be.equal(erc721.address) + s[3].should.be.eq.BN(itemPrice) + s[4].should.be.eq.BN(endTime) + }) + + it('should create a new order :: Relayed EIP721', async function() { + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'priceInWei', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'expiresAt', + type: 'uint256', + }, + ], + name: 'createOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId, itemPrice, endTime] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + + checkOrderCreatedLogs( + [logs[1]], + assetId, + seller, + erc721.address, + itemPrice, + endTime + ) + + // Check data + let s = await market.orderByAssetId.call(erc721.address, assetId) + s[1].toLowerCase().should.be.equal(seller.toLowerCase()) + s[2].toLowerCase().should.be.equal(erc721.address.toLowerCase()) + s[3].should.be.eq.BN(itemPrice) + s[4].should.be.eq.BN(endTime) + }) + + it('should update an order', async function() { + let newPrice = web3.utils.toWei('2.0', 'ether') + let newEndTime = endTime + duration.minutes(5) + + const { logs } = await createOrder( + erc721.address, + assetId, + newPrice, + newEndTime, + { from: seller } + ) + + logs.length.should.be.equal(1) + checkOrderCreatedLogs( + logs, + assetId, + seller, + erc721.address, + newPrice, + newEndTime + ) + + // Check data + let s = await market.orderByAssetId.call(erc721.address, assetId) + s[1].should.be.equal(seller) + s[2].should.be.equal(erc721.address) + s[3].should.be.eq.BN(newPrice) + s[4].should.be.eq.BN(newEndTime) + }) + + it('should update an order :: Relayed EIP721', async function() { + let newPrice = web3.utils.toWei('2.0', 'ether') + let newEndTime = endTime + duration.minutes(5) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'priceInWei', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'expiresAt', + type: 'uint256', + }, + ], + name: 'createOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId, newPrice, newEndTime] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + + checkOrderCreatedLogs( + [logs[1]], + assetId, + seller, + erc721.address, + newPrice, + newEndTime + ) + + // Check data + let s = await market.orderByAssetId.call(erc721.address, assetId) + s[1].should.be.equal(seller) + s[2].should.be.equal(erc721.address) + s[3].should.be.eq.BN(newPrice) + s[4].should.be.eq.BN(newEndTime) + }) + + it('should fail to create an order :: (contract not approved)', async function() { + const newAssetId = 123123123 + await erc721.mint(otherAddress, newAssetId) + + await assertRevert( + createOrder(erc721.address, newAssetId, itemPrice, endTime, { + from: seller, + }) + ) + }) + + it('should fail to create an order :: (address not the owner of asset)', async function() { + await assertRevert( + createOrder(erc721.address, assetId, itemPrice, endTime, { + from: otherAddress, + }), + 'MarketplaceV2#_createOrder: NOT_ASSET_OWNER' + ) + }) + + it('should fail to create an order :: (address not the owner of asset) :: Relayed EIP721', async function() { + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'priceInWei', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'expiresAt', + type: 'uint256', + }, + ], + name: 'createOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId, itemPrice, endTime] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + otherAddress, + relayer, + null, + domain, + version + ) + ) + }) + + it('should fail to create an order :: (price is 0)', async function() { + await assertRevert( + market.createOrder(erc721.address, assetId, 0, endTime, { + from: seller, + }), + EVMRevert + ) + }) + + it('should fail to create an order :: (expires too soon)', async function() { + const newTime = + (await web3.eth.getBlock('latest')).timestamp + duration.seconds(59) + await assertRevert( + market.createOrder(erc721.address, assetId, itemPrice, newTime, { + from: seller, + }), + 'MarketplaceV2#_createOrder: INVALID_EXPIRES_AT' + ) + }) + + it('should fail to create an order :: (nft not approved)', async function() { + await erc721.setApprovalForAll(market.address, false, { from: seller }) + await assertRevert( + market.createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + ) + }) + + it('should fail to create an order :: (publication fee not paid)', async function() { + await erc20.approve(market.address, 1, { from: seller }) + await market.setPublicationFee(2, { from: owner }) + await assertRevert( + market.createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }), + 'ERC20: transfer amount exceeds allowance' + ) + }) + }) + + describe('Cancel', function() { + it('should let the seller cancel a created order', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + const { logs } = await cancelOrder(erc721.address, assetId, { + from: seller, + }) + + logs.length.should.be.equal(1) + checkOrderCancelledLogs(logs, assetId, seller, erc721.address) + }) + + it('should let the seller cancel a created order :: Relayed EIP721', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + ], + name: 'cancelOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + checkOrderCancelledLogs([logs[1]], assetId, seller, erc721.address) + }) + + it('should let the contract owner cancel a created order', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + const { logs } = await cancelOrder(erc721.address, assetId, { + from: owner, + }) + + logs.length.should.be.equal(1) + checkOrderCancelledLogs(logs, assetId, seller, erc721.address) + }) + + it('should let the contract owner cancel a created order :: Relayed EIP721', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + ], + name: 'cancelOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + owner, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + checkOrderCancelledLogs([logs[1]], assetId, seller, erc721.address) + }) + + it('should fail canceling an order :: (wrong user)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await assertRevert( + cancelOrder(erc721.address, assetId, { + from: buyer, + }), + 'MarketplaceV2#_cancelOrder: UNAUTHORIZED_USER' + ) + }) + + it('should fail canceling an order :: (wrong user) :: Relayed EIP721', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + ], + name: 'cancelOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + buyer, + relayer, + null, + domain, + version + ) + ) + }) + + it('should fail canceling an order :: (wrong NFT address)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await assertRevert( + cancelOrder(erc20.address, assetId, { + from: seller, + }), + 'MarketplaceV2#_cancelOrder: INVALID_ORDER' + ) + }) + + it('should fail canceling an order :: (double cancel)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await cancelOrder(erc721.address, assetId, { from: seller }) + + await assertRevert( + cancelOrder(erc721.address, assetId, { + from: seller, + }), + 'MarketplaceV2#_cancelOrder: INVALID_ORDER' + ) + }) + }) + + describe('Execute', function() { + it('should execute a created order', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + const { logs } = await executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }) + + logs.length.should.be.equal(1) + checkOrderSuccessfulLogs( + logs, + assetId, + seller, + erc721.address, + itemPrice, + buyer + ) + }) + + it('should execute a created order :: Relayed EIP721', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'price', + type: 'uint256', + }, + ], + name: 'executeOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId, itemPrice] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + buyer, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + checkOrderSuccessfulLogs( + logs, + assetId, + seller, + erc721.address, + itemPrice, + buyer + ) + }) + + it('should fail on execute a created order :: (wrong user)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + await assertRevert( + executeOrder(erc721.address, assetId, itemPrice, { + from: seller, + }), + 'MarketplaceV2#_executeOrder: SENDER_IS_SELLER' + ) + }) + + it('should fail on execute a created order :: (wrong NFT address)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + }) + + it('should fail execute a created order :: (expired)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + // move an hour ahead + await increaseTime(3600) + await assertRevert( + executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }), + 'MarketplaceV2#_executeOrder: ORDER_EXPIRED' + ) + }) + + it('should fail on execute a created order :: (double execute)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }) + + await assertRevert( + executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }), + 'MarketplaceV2#_executeOrder: ASSET_NOT_FOR_SALE' + ) + }) + + it('should fail to execute a created order :: (not an ERC721 contract)', async function() { + await assertRevert( + createOrder(erc20.address, assetId, itemPrice, endTime, { + from: seller, + }) + ) + }) + + it('should fail to execute a created order :: (price mismatch)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await assertRevert( + executeOrder(erc721.address, assetId, itemPrice2, { + from: buyer, + }), + 'MarketplaceV2#_executeOrder: PRICE_MISMATCH' + ) + }) + + it('should fail to execute a created order :: (seller is not the owner)', async function() { + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + await erc721.transferFrom(seller, anotherUser, assetId, { + from: seller, + }) + + await assertRevert( + executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }), + 'MarketplaceV2#_executeOrder: SELLER_NOT_OWNER' + ) + }) + + it('should fail on execute a created order :: (cant pay for order)', async function() { + const balance = await erc20.balanceOf(buyer) + await erc20.transfer(owner, balance, { from: buyer }) + await erc20.setBalance(buyer, web3.utils.toWei('9.9', 'ether')) + + await createOrder( + erc721.address, + assetId, + web3.utils.toWei('10', 'ether'), + endTime, + { + from: seller, + } + ) + + await assertRevert( + executeOrder(erc721.address, assetId, web3.utils.toWei('10', 'ether'), { + from: buyer, + }), + 'ERC20: transfer amount exceeds balance' + ) + }) + + it('should fail on execute a created order :: (cant pay for collector fees)', async function() { + const balance = await erc20.balanceOf(buyer) + await erc20.transfer(owner, balance, { from: buyer }) + await erc20.setBalance(buyer, web3.utils.toWei('0', 'ether')) + + await market.setFeesCollectorCutPerMillion(10000, { from: owner }) + + await createOrder( + erc721.address, + assetId, + web3.utils.toWei('10.0', 'ether'), + endTime, + { + from: seller, + } + ) + + await assertRevert( + executeOrder(erc721.address, assetId, web3.utils.toWei('10', 'ether'), { + from: buyer, + }), + 'ERC20: transfer amount exceeds balance' + ) + }) + + it('should fail on execute a created order :: (cant pay for royalties fees)', async function() { + const balance = await erc20.balanceOf(buyer) + await erc20.transfer(owner, balance, { from: buyer }) + await erc20.setBalance(buyer, web3.utils.toWei('1', 'ether')) + + await market.setFeesCollectorCutPerMillion(10000, { from: owner }) + await market.setRoyaltiesCutPerMillion(10000, { from: owner }) + + await createOrder( + erc721.address, + assetId, + web3.utils.toWei('10.0', 'ether'), + endTime, + { + from: seller, + } + ) + + await assertRevert( + executeOrder(erc721.address, assetId, web3.utils.toWei('10', 'ether'), { + from: buyer, + }), + 'ERC20: transfer amount exceeds balance' + ) + }) + }) + + describe('Safe Execute', function() { + beforeEach(async () => { + fingerprint = await verifiableErc721.getFingerprint(0) + fingerprint = fingerprint.toString() + }) + + it('should verify and execute a created order', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + const { logs } = await market.safeExecuteOrder( + verifiableErc721.address, + assetId, + itemPrice, + fingerprint, + { from: buyer } + ) + + logs.length.should.be.equal(1) + checkOrderSuccessfulLogs( + logs, + assetId, + seller, + verifiableErc721.address, + itemPrice, + buyer + ) + }) + + it('should verify and execute a created order :: Relayed EIP721', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'price', + type: 'uint256', + }, + { + internalType: 'bytes', + name: 'fingerprint', + type: 'bytes', + }, + ], + name: 'safeExecuteOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [verifiableErc721.address, assetId, itemPrice, fingerprint] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + buyer, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + checkOrderSuccessfulLogs( + logs, + assetId, + seller, + verifiableErc721.address, + itemPrice, + buyer + ) + }) + + it('should fail on execute a created order :: (wrong fingerprint)', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + await assertRevert( + market.safeExecuteOrder( + verifiableErc721.address, + assetId, + itemPrice, + web3.utils.randomHex(32), + { + from: seller, + } + ), + 'MarketplaceV2#_executeOrder: INVALID_FINGERPRINT' + ) + }) + + it('should fail on execute a created order :: (wrong user)', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + await assertRevert( + market.safeExecuteOrder( + verifiableErc721.address, + assetId, + itemPrice, + fingerprint, + { from: seller } + ), + 'MarketplaceV2#_executeOrder: SENDER_IS_SELLER' + ) + }) + + it('should fail on unsafe executeOrder :: (verifiable NFT registry)', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await assertRevert( + market.executeOrder(verifiableErc721.address, assetId, itemPrice, { + from: buyer, + }), + 'MarketplaceV2#_executeOrder: INVALID_FINGERPRINT' + ) + }) + + it('should fail execute a created order :: (expired)', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + // move an hour ahead + await increaseTime(3600) + await assertRevert( + market.safeExecuteOrder( + verifiableErc721.address, + assetId, + itemPrice, + fingerprint, + { from: buyer } + ), + 'MarketplaceV2#_executeOrder: ORDER_EXPIRED' + ) + }) + + it('should fail to execute a created order :: (price mismatch)', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + await assertRevert( + market.safeExecuteOrder( + verifiableErc721.address, + assetId, + itemPrice2, + fingerprint, + { from: buyer } + ), + 'MarketplaceV2#_executeOrder: PRICE_MISMATCH' + ) + }) + + it('should fail to execute a created order :: (seller is not the owner)', async function() { + await createOrder(verifiableErc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + await verifiableErc721.transferFrom(seller, anotherUser, assetId, { + from: seller, + }) + + await assertRevert( + market.safeExecuteOrder( + verifiableErc721.address, + assetId, + itemPrice, + fingerprint, + { from: buyer } + ), + 'MarketplaceV2#_executeOrder: SELLER_NOT_OWNER' + ) + }) + }) + + describe('setPublicationFee', function() { + it('should be initialized to 0', async function() { + const response = await market.publicationFeeInWei() + response.should.be.eq.BN(0) + }) + + it('should change publication fee', async function() { + let publicationFee = web3.utils.toWei('0.005', 'ether') + + const { logs } = await market.setPublicationFee(publicationFee, { + from: owner, + }) + let response = await market.publicationFeeInWei() + response.should.be.eq.BN(publicationFee) + logs.length.should.be.equal(1) + checkChangedPublicationFeeLog(logs[0], publicationFee) + + await market.setPublicationFee(0, { + from: owner, + }) + + response = await market.publicationFeeInWei() + response.should.be.eq.BN(0) + }) + + it('should change publication fee :: Relayed EIP721', async function() { + let publicationFee = web3.utils.toWei('0.005', 'ether') + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'uint256', + name: '_publicationFee', + type: 'uint256', + }, + ], + name: 'setPublicationFee', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [publicationFee] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + owner, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + + let response = await market.publicationFeeInWei() + response.should.be.eq.BN(publicationFee) + logs.length.should.be.equal(1) + checkChangedPublicationFeeLog(logs[0], publicationFee) + }) + + it('should fail to change publication fee (not owner)', async function() { + const publicationFee = web3.utils.toWei('0.005', 'ether') + + await assertRevert( + market.setPublicationFee(publicationFee, { from: seller }), + 'Ownable: caller is not the owner' + ) + }) + + it('should fail to change publication fee (not owner) :: Relayed EIP721', async function() { + let publicationFee = web3.utils.toWei('0.005', 'ether') + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'uint256', + name: '_publicationFee', + type: 'uint256', + }, + ], + name: 'setPublicationFee', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [publicationFee] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + ) + }) + }) + + describe('setFeesCollector', function() { + it('should change fee collector', async function() { + let _feesCollector = await market.feesCollector() + expect(_feesCollector).to.be.equal(feesCollector) + + const { logs } = await market.setFeesCollector(anotherUser, { + from: owner, + }) + + _feesCollector = await market.feesCollector() + expect(_feesCollector).to.be.equal(anotherUser) + + logs.length.should.be.equal(1) + checkFeesCollectorSetLog(logs[0], feesCollector, anotherUser) + + await market.setFeesCollector(feesCollector, { + from: owner, + }) + + _feesCollector = await market.feesCollector() + expect(_feesCollector).to.be.equal(feesCollector) + }) + + it('should change fee collector :: Relayed EIP721', async function() { + let _feesCollector = await market.feesCollector() + expect(_feesCollector).to.be.equal(feesCollector) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: '_feesCollector', + type: 'address', + }, + ], + name: 'setFeesCollector', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [anotherUser] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + owner, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + + _feesCollector = await market.feesCollector() + expect(_feesCollector).to.be.equal(anotherUser) + + logs.length.should.be.equal(1) + checkFeesCollectorSetLog(logs[0], feesCollector, anotherUser) + }) + + it('should fail to change fee collector address zero', async function() { + await assertRevert( + market.setFeesCollector(zeroAddress, { from: owner }), + 'MarketplaceV2#setFeesCollector: INVALID_FEES_COLLECTOR' + ) + }) + + it('should fail to change fee collector (not owner)', async function() { + await assertRevert( + market.setFeesCollector(anotherUser, { from: seller }), + 'Ownable: caller is not the owner' + ) + }) + + it('should fail to change fee collector (not owner) :: Relayed EIP721', async function() { + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: '_feesCollector', + type: 'address', + }, + ], + name: 'setFeesCollector', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [anotherUser] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + ) + }) + }) + + describe('setRoyaltiesManager', function() { + it('should change royalties manager', async function() { + let _royaltiesManager = await market.royaltiesManager() + expect(_royaltiesManager).to.be.equal(royaltiesManager.address) + + const { logs } = await market.setRoyaltiesManager(erc721.address, { + from: owner, + }) + + _royaltiesManager = await market.royaltiesManager() + expect(_royaltiesManager).to.be.equal(erc721.address) + + logs.length.should.be.equal(1) + checkRoyaltiesManagerSetLog( + logs[0], + royaltiesManager.address, + erc721.address + ) + + await market.setRoyaltiesManager(royaltiesManager.address, { + from: owner, + }) + + _royaltiesManager = await market.royaltiesManager() + expect(_royaltiesManager).to.be.equal(royaltiesManager.address) + }) + + it('should change royalties manager :: Relayed EIP721', async function() { + let _royaltiesManager = await market.royaltiesManager() + expect(_royaltiesManager).to.be.equal(royaltiesManager.address) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: '_royaltiesManager', + type: 'address', + }, + ], + name: 'setRoyaltiesManager', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + owner, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + + _royaltiesManager = await market.royaltiesManager() + expect(_royaltiesManager).to.be.equal(erc721.address) + + logs.length.should.be.equal(1) + checkRoyaltiesManagerSetLog( + logs[0], + royaltiesManager.address, + erc721.address + ) + }) + + it('should fail to change royalties manager address zero', async function() { + await assertRevert( + market.setRoyaltiesManager(zeroAddress, { from: owner }), + 'MarketplaceV2#setRoyaltiesManager: INVALID_ROYALTIES_MANAGER' + ) + }) + + it('should fail to change royalties manager to a not contract', async function() { + await assertRevert( + market.setRoyaltiesManager(anotherUser, { from: owner }), + 'MarketplaceV2#setRoyaltiesManager: INVALID_ROYALTIES_MANAGER' + ) + }) + + it('should fail to change royalties manager (not owner)', async function() { + await assertRevert( + market.setRoyaltiesManager(erc721.address, { from: seller }), + 'Ownable: caller is not the owner' + ) + }) + + it('should fail to change royalties manager (not owner) :: Relayed EIP721', async function() { + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: '_royaltiesManager', + type: 'address', + }, + ], + name: 'setRoyaltiesManager', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [anotherUser] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + ) + }) + }) + + describe('feesCollectorCutPerMillion', function() { + it('should be initialized to 0', async function() { + const response = await market.feesCollectorCutPerMillion() + response.should.be.eq.BN(0) + }) + + it('should change fee collector sale cut', async function() { + const feesCollectorCut = 10 + + const { logs } = await market.setFeesCollectorCutPerMillion( + feesCollectorCut, + { + from: owner, + } + ) + let response = await market.feesCollectorCutPerMillion() + response.should.be.eq.BN(feesCollectorCut) + logs.length.should.be.equal(1) + checkChangedFeesCollectorCutPerMillionLog(logs[0], feesCollectorCut) + + await market.setFeesCollectorCutPerMillion(0, { + from: owner, + }) + response = await market.feesCollectorCutPerMillion() + response.should.be.eq.BN(0) + }) + + it('should change fee collector sale cut :: Relayed EIP721', async function() { + const feesCollectorCut = 10 + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'uint256', + name: '_feesCollectorCutPerMillion', + type: 'uint256', + }, + ], + name: 'setFeesCollectorCutPerMillion', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [feesCollectorCut] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + owner, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + + let response = await market.feesCollectorCutPerMillion() + response.should.be.eq.BN(feesCollectorCut) + logs.length.should.be.equal(1) + checkChangedFeesCollectorCutPerMillionLog(logs[0], feesCollectorCut) + }) + + it('should fail to change fee collector cut (% invalid above)', async function() { + await assertRevert( + market.setFeesCollectorCutPerMillion(10000000, { from: owner }), + 'MarketplaceV2#setFeesCollectorCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + + it('should fail to change fee collector cut (% invalid above along with royalties cut)', async function() { + await market.setRoyaltiesCutPerMillion(1, { from: owner }) + + await assertRevert( + market.setFeesCollectorCutPerMillion(999999, { from: owner }), + 'MarketplaceV2#setFeesCollectorCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + + it('should fail to change fee collector cut (not owner)', async function() { + const feesCollectorCut = 10 + + await assertRevert( + market.setFeesCollectorCutPerMillion(feesCollectorCut, { + from: seller, + }), + 'Ownable: caller is not the owner' + ) + }) + + it('should fail to change fee collector cut (not owner) :: Relayed EIP721', async function() { + const feesCollectorCut = 10 + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'uint256', + name: '_feesCollectorCutPerMillion', + type: 'uint256', + }, + ], + name: 'setFeesCollectorCutPerMillion', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [feesCollectorCut] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + ) + }) + }) + + describe('royaltiesCutPerMillion', function() { + it('should be initialized to 0', async function() { + const response = await market.royaltiesCutPerMillion() + response.should.be.eq.BN(0) + }) + + it('should change royalties cut', async function() { + const royaltiesCut = 10 + + const { logs } = await market.setRoyaltiesCutPerMillion(royaltiesCut, { + from: owner, + }) + let response = await market.royaltiesCutPerMillion() + response.should.be.eq.BN(royaltiesCut) + logs.length.should.be.equal(1) + checkChangedRoyaltiesCutPerMillionLog(logs[0], royaltiesCut) + + await market.setRoyaltiesCutPerMillion(0, { + from: owner, + }) + response = await market.royaltiesCutPerMillion() + response.should.be.eq.BN(0) + }) + + it('should change royalties cut :: Relayed EIP721', async function() { + const royaltiesCut = 10 + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'uint256', + name: '_royaltiesCutPerMillion', + type: 'uint256', + }, + ], + name: 'setRoyaltiesCutPerMillion', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [royaltiesCut] + ) + + const { logs } = await sendMetaTx( + market, + functionSignature, + owner, + relayer, + null, + domain, + version + ) + + logs.length.should.be.equal(2) + logs.shift() + + let response = await market.royaltiesCutPerMillion() + response.should.be.eq.BN(royaltiesCut) + logs.length.should.be.equal(1) + checkChangedRoyaltiesCutPerMillionLog(logs[0], royaltiesCut) + }) + + it('should fail to change royalties cut (% invalid above)', async function() { + await assertRevert( + market.setRoyaltiesCutPerMillion(10000000, { from: owner }), + 'MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + + it('should fail to change royalties cut (% invalid above along with fee collector cut)', async function() { + await market.setFeesCollectorCutPerMillion(1, { from: owner }) + + await assertRevert( + market.setRoyaltiesCutPerMillion(999999, { from: owner }), + 'MarketplaceV2#setRoyaltiesCutPerMillion: TOTAL_FEES_MUST_BE_BETWEEN_0_AND_999999' + ) + }) + + it('should fail to change royalties cut (not owner)', async function() { + const royaltiesCut = 10 + + await assertRevert( + market.setRoyaltiesCutPerMillion(royaltiesCut, { from: seller }), + 'Ownable: caller is not the owner' + ) + }) + + it('should fail to change royalties cut (not owner) :: Relayed EIP721', async function() { + const royaltiesCut = 10 + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'uint256', + name: '_royaltiesCutPerMillion', + type: 'uint256', + }, + ], + name: 'setRoyaltiesCutPerMillion', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [royaltiesCut] + ) + + await assertRevert( + sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + ) + }) + }) + + describe('Create with publication fee', function() { + it('should publish with fee', async function() { + const balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + + let publicationFee = web3.utils.toWei('0.5', 'ether') + + await market.setPublicationFee(publicationFee, { from: owner }) + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('9.5', 'ether')) + + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.5', 'ether')) + }) + + it('should publish with fee :: Relayed EIP721', async function() { + const balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + + let publicationFee = web3.utils.toWei('0.5', 'ether') + + await market.setPublicationFee(publicationFee, { from: owner }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'priceInWei', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'expiresAt', + type: 'uint256', + }, + ], + name: 'createOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId, itemPrice, endTime] + ) + + await sendMetaTx( + market, + functionSignature, + seller, + relayer, + null, + domain, + version + ) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('9.5', 'ether')) + + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.5', 'ether')) + }) + }) + + describe('Create with cut', function() { + it('should sell with fees collector sale cut', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let feesCollectorCut = 100000 + + await market.setFeesCollectorCutPerMillion(feesCollectorCut, { + from: owner, + }) + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.9', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell with fees collector sale cut :: Relayed EIP721', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let feesCollectorCut = 100000 + + await market.setFeesCollectorCutPerMillion(feesCollectorCut, { + from: owner, + }) + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'price', + type: 'uint256', + }, + ], + name: 'executeOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721.address, assetId, itemPrice] + ) + + await sendMetaTx( + market, + functionSignature, + buyer, + relayer, + null, + domain, + version + ) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.9', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell with royalties collector sale cut (item beneficiary)', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let royaltiesCutPerMillion = 100000 + + await market.setRoyaltiesCutPerMillion(royaltiesCutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.9', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell with fees collector sale cut (item beneficiary) :: Relayed EIP721', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let royaltiesCutPerMillion = 100000 + + await market.setRoyaltiesCutPerMillion(royaltiesCutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, assetId, itemPrice, endTime, { + from: seller, + }) + + const functionSignature = web3.eth.abi.encodeFunctionCall( + { + inputs: [ + { + internalType: 'address', + name: 'nftAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assetId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'price', + type: 'uint256', + }, + ], + name: 'executeOrder', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + [erc721Collection.address, assetId, itemPrice] + ) + + await sendMetaTx( + market, + functionSignature, + buyer, + relayer, + null, + domain, + version + ) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.9', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell with royalties collector sale cut (item creator)', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let royaltiesCutPerMillion = 100000 + + await market.setRoyaltiesCutPerMillion(royaltiesCutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, 0, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, 0, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.9', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell and send to fees collector if the royalties receiver is the zero address', async function() { + await erc721Collection.setCreator(zeroAddress) + await erc721Collection.setBeneficiary(zeroAddress) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let royaltiesCutPerMillion = 100000 + + await market.setRoyaltiesCutPerMillion(royaltiesCutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, 0, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, 0, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.9', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell and compute both fees collector and royalties cut to the fees collector if royalties beneficiary is the zero address', async function() { + await erc721Collection.setCreator(zeroAddress) + await erc721Collection.setBeneficiary(zeroAddress) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let cutPerMillion = 100000 + + await market.setFeesCollectorCutPerMillion(cutPerMillion, { + from: owner, + }) + await market.setRoyaltiesCutPerMillion(cutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, 0, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, 0, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.2', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.8', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell and compute both fees collector and royalties cut to the fees collector if the NFT is not a collection interface compliant', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let cutPerMillion = 100000 + + await market.setFeesCollectorCutPerMillion(cutPerMillion, { + from: owner, + }) + await market.setRoyaltiesCutPerMillion(cutPerMillion, { + from: owner, + }) + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.2', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.8', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell and compute both fees collector and royalties cut to the fees collector if royalties manager is invalid (not collection compliant)', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + await market.setRoyaltiesManager(erc721.address, { from: owner }) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let cutPerMillion = 100000 + + await market.setFeesCollectorCutPerMillion(cutPerMillion, { + from: owner, + }) + await market.setRoyaltiesCutPerMillion(cutPerMillion, { + from: owner, + }) + await createOrder(erc721.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.2', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.8', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell and compute both fees collector and royalties cut to the fees collector if royalties manager is invalid (collection compliant)', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + await market.setRoyaltiesManager(erc721.address, { from: owner }) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let cutPerMillion = 100000 + + await market.setFeesCollectorCutPerMillion(cutPerMillion, { + from: owner, + }) + await market.setRoyaltiesCutPerMillion(cutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.2', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.8', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + + it('should sell and compute both fees', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + let cutPerMillion = 100000 + + await market.setFeesCollectorCutPerMillion(cutPerMillion, { + from: owner, + }) + await market.setRoyaltiesCutPerMillion(cutPerMillion, { + from: owner, + }) + await createOrder(erc721Collection.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.1', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('10.8', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + it('should sell without fees', async function() { + await erc721Collection.setCreator(itemCreator) + await erc721Collection.setBeneficiary(itemBeneficiary) + + let balance = await erc20.balanceOf(seller) + await erc20.transfer(otherAddress, balance, { from: seller }) + + balance = await erc20.balanceOf(buyer) + await erc20.transfer(otherAddress, balance, { from: buyer }) + + // Set token balances + await erc20.setBalance(feesCollector, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(buyer, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(seller, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemCreator, web3.utils.toWei('10.0', 'ether')) + await erc20.setBalance(itemBeneficiary, web3.utils.toWei('10.0', 'ether')) + + await createOrder(erc721Collection.address, assetId, itemPrice, endTime, { + from: seller, + }) + await executeOrder(erc721Collection.address, assetId, itemPrice, { + from: buyer, + }) + + // Verify balances + let feesCollectorBalance = await erc20.balanceOf(feesCollector) + feesCollectorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemCreatorBalance = await erc20.balanceOf(itemCreator) + itemCreatorBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let itemBeneficiaryBalance = await erc20.balanceOf(itemBeneficiary) + itemBeneficiaryBalance.should.be.eq.BN(web3.utils.toWei('10.0', 'ether')) + + let sellerBalance = await erc20.balanceOf(seller) + sellerBalance.should.be.eq.BN(web3.utils.toWei('11.0', 'ether')) + + let buyerBalance = await erc20.balanceOf(buyer) + buyerBalance.should.be.eq.BN(web3.utils.toWei('9.0', 'ether')) + }) + }) +}) diff --git a/test/helpers/assertRevert.js b/test/helpers/assertRevert.js new file mode 100644 index 0000000..a2dfc14 --- /dev/null +++ b/test/helpers/assertRevert.js @@ -0,0 +1,16 @@ +const should = require('chai').should() + +export async function assertRevert(promise, message) { + try { + await promise + } catch (error) { + const withMessage = message ? message : 'revert' + + error.message.should.include( + withMessage, + `Expected "revert", got ${error} instead` + ) + return + } + should.fail('Expected revert not received') +}