diff --git a/packages/marketplace/src/exchange/AssetMatcher.md b/packages/marketplace/src/exchange/AssetMatcher.md new file mode 100644 index 0000000000..01898dec50 --- /dev/null +++ b/packages/marketplace/src/exchange/AssetMatcher.md @@ -0,0 +1,14 @@ +#### Features + +`matchAssets` function should calculate if Asset types match with each other. + +Simple asset types match if they are equal, for example, ERC-20 token with address `address1` match to ERC-20 token with address `address1`, but doesn't match any ERC-721 token or ERC-20 token with `address2`. + +There can be asset types which can't be compared to other asset types directly. For example, imagine Asset type `any Decentraland Land`. This asset type is not equal `Decentraland Land X` (with specific tokenId), but new `IAssetMatcher` (see [here](./IAssetMatcher.sol)) can be registered in this contract. + +New registered `IAssetMatcher` will be responsible for matching `any Decentraland Land` with `Decentraland Land X`. + +Assets match if +- it's a simple asset and it equals asset from the other side +- registered IAssetMatcher returns match +- otherwise assets match if asset classes match and their data matches (we calculate hash and compare hashes) \ No newline at end of file diff --git a/packages/marketplace/src/exchange/AssetMatcher.sol b/packages/marketplace/src/exchange/AssetMatcher.sol new file mode 100644 index 0000000000..6d718fabb1 --- /dev/null +++ b/packages/marketplace/src/exchange/AssetMatcher.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IAssetMatcher} from "../interfaces/IAssetMatcher.sol"; +import {LibAsset} from "../lib-asset/LibAsset.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title AssetMatcher contract +/// @notice matchAssets function should calculate if Asset types match with each other +contract AssetMatcher is Ownable, IAssetMatcher { + bytes internal constant EMPTY = ""; + mapping(bytes4 => address) internal matchers; + + /// @notice event emitted when an AssetMacher is set + /// @param assetType represented by bytes4 + /// @param matcher address of the matcher + event MatcherChange(bytes4 indexed assetType, address indexed matcher); + + /// @notice set AssetMacher + /// @param assetType to be matched by the matcher contract + /// @param matcher address of the matcher + function setAssetMatcher(bytes4 assetType, address matcher) external onlyOwner { + matchers[assetType] = matcher; + emit MatcherChange(assetType, matcher); + } + + /// @notice calculate if Asset types match with each other + /// @param leftAssetType to be matched with rightAssetType + /// @param rightAssetType to be matched with leftAssetType + /// @return AssetType of the match + function matchAssets( + LibAsset.AssetType memory leftAssetType, + LibAsset.AssetType memory rightAssetType + ) external view returns (LibAsset.AssetType memory) { + LibAsset.AssetType memory result = matchAssetOneSide(leftAssetType, rightAssetType); + if (result.assetClass == 0) { + return matchAssetOneSide(rightAssetType, leftAssetType); + } else { + return result; + } + } + + function matchAssetOneSide( + LibAsset.AssetType memory leftAssetType, + LibAsset.AssetType memory rightAssetType + ) private view returns (LibAsset.AssetType memory) { + bytes4 classLeft = leftAssetType.assetClass; + bytes4 classRight = rightAssetType.assetClass; + if (classLeft == LibAsset.ETH_ASSET_CLASS) { + if (classRight == LibAsset.ETH_ASSET_CLASS) { + return leftAssetType; + } + return LibAsset.AssetType(0, EMPTY); + } + if (classLeft == LibAsset.ERC20_ASSET_CLASS) { + if (classRight == LibAsset.ERC20_ASSET_CLASS) { + return simpleMatch(leftAssetType, rightAssetType); + } + return LibAsset.AssetType(0, EMPTY); + } + if (classLeft == LibAsset.ERC721_ASSET_CLASS) { + if (classRight == LibAsset.ERC721_ASSET_CLASS) { + return simpleMatch(leftAssetType, rightAssetType); + } + return LibAsset.AssetType(0, EMPTY); + } + if (classLeft == LibAsset.ERC1155_ASSET_CLASS) { + if (classRight == LibAsset.ERC1155_ASSET_CLASS) { + return simpleMatch(leftAssetType, rightAssetType); + } + return LibAsset.AssetType(0, EMPTY); + } + if (classLeft == LibAsset.BUNDLE) { + if (classRight == LibAsset.BUNDLE) { + return simpleMatch(leftAssetType, rightAssetType); + } + return LibAsset.AssetType(0, EMPTY); + } + address matcher = matchers[classLeft]; + if (matcher != address(0)) { + return IAssetMatcher(matcher).matchAssets(leftAssetType, rightAssetType); + } + if (classLeft == classRight) { + return simpleMatch(leftAssetType, rightAssetType); + } + revert("not found IAssetMatcher"); + } + + function simpleMatch( + LibAsset.AssetType memory leftAssetType, + LibAsset.AssetType memory rightAssetType + ) private pure returns (LibAsset.AssetType memory) { + bytes32 leftHash = keccak256(leftAssetType.data); + bytes32 rightHash = keccak256(rightAssetType.data); + if (leftHash == rightHash) { + return leftAssetType; + } + return LibAsset.AssetType(0, EMPTY); + } +} diff --git a/packages/marketplace/src/exchange/Exchange.sol b/packages/marketplace/src/exchange/Exchange.sol new file mode 100644 index 0000000000..02ff967f6f --- /dev/null +++ b/packages/marketplace/src/exchange/Exchange.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ExchangeCore} from "./ExchangeCore.sol"; +import {TransferManager, IRoyaltiesProvider} from "../transfer-manager/TransferManager.sol"; + +/// @title Exchange contract +/// @notice Used to exchange assets, that is, tokens. +/// @dev Main functions are in ExchangeCore +/// @dev TransferManager is used to execute token transfers +contract Exchange is ExchangeCore, TransferManager { + /// @notice Exchange contract initializer + /// @param newProtocolFeePrimary protocol fee applied for primary markets + /// @param newProtocolFeeSecondary protocol fee applied for secondary markets + /// @param newDefaultFeeReceiver market fee receiver + /// @param newRoyaltiesProvider registry for the different types of royalties + /// @param orderValidatorAdress address of the OrderValidator contract, that validates orders + /// @param newNativeOrder bool to indicate of the contract accepts or doesn't native tokens, i.e. ETH or Matic + /// @param newMetaNative same as =nativeOrder but for metaTransactions + function __Exchange_init( + uint256 newProtocolFeePrimary, + uint256 newProtocolFeeSecondary, + address newDefaultFeeReceiver, + IRoyaltiesProvider newRoyaltiesProvider, + address orderValidatorAdress, + bool newNativeOrder, + bool newMetaNative + ) external initializer { + __Ownable_init(); + __ExchangeCoreInitialize(newNativeOrder, newMetaNative, orderValidatorAdress); + __TransferManager_init_unchained( + newProtocolFeePrimary, + newProtocolFeeSecondary, + newDefaultFeeReceiver, + newRoyaltiesProvider + ); + } +} diff --git a/packages/marketplace/src/exchange/ExchangeCore.md b/packages/marketplace/src/exchange/ExchangeCore.md new file mode 100644 index 0000000000..01dcc1ec6e --- /dev/null +++ b/packages/marketplace/src/exchange/ExchangeCore.md @@ -0,0 +1,28 @@ +#### Features + +The file contains a list of widely used functions: `cancel`, `matchOrders`, `directPurchase`, `directAcceptBid`. + +##### Algorithm `cancel(Order order)` + +Two main requirements for function are: + - If msg sender is `order.maker`, + - Order.salt not equal to 0. + +`Order` hash is calculated using [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md). Value equal to max uint256 value is set to map `fills` with key == `Order` hash. + +##### Algorithm `matchOrders(Order orderLeft, bytes signatureLeft, Order orderRight, bytes signatureRight)` + +Orders are being validated by `validateOrders()` internal function, if error is find, function being reverted. + +Next step is `matchAssets` function, should calculate if Asset types match with each other. + +Next step is parsing `Order.data`. After that function `setFillEmitMatch()` calculates fills for the matched orders and set them in "fills" mapping. Finally `doTransfers()` function is called. + +##### Algorithm `directPurchase(Purchase direct)` or `directAcceptBid(AcceptBid direct)` + +We recommend use `directPurchase` and `directAcceptBid` methods for reducing the gas consumption. Data from the structures `Purchase` and `AcceptBid` form purchase and sell Orders. Orders are being validated by `validateOrders()` internal function. Further Orders serve as parameters of the function `matchAndTransfer()`. + +##### Contract relationship + +For better understanding how contracts interconnect and what functions are used, see picture: +![Relationship1](documents/diagram-13983673345763902680.png) \ No newline at end of file diff --git a/packages/marketplace/src/exchange/ExchangeCore.sol b/packages/marketplace/src/exchange/ExchangeCore.sol new file mode 100644 index 0000000000..3a1e24a5e2 --- /dev/null +++ b/packages/marketplace/src/exchange/ExchangeCore.sol @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibFill} from "./libraries/LibFill.sol"; +import {LibDirectTransfer} from "./libraries/LibDirectTransfer.sol"; +import {LibOrderDataGeneric, LibOrder, LibOrderData} from "./libraries/LibOrderDataGeneric.sol"; +import {LibDeal, LibFeeSide, LibPart, LibAsset} from "../transfer-manager/lib/LibDeal.sol"; +import {TransferExecutor, Initializable, OwnableUpgradeable, LibTransfer} from "../transfer-manager/TransferExecutor.sol"; + +import {IAssetMatcher} from "../interfaces/IAssetMatcher.sol"; +import {ITransferManager} from "../transfer-manager/interfaces/ITransferManager.sol"; +import {IOrderValidator} from "../interfaces/IOrderValidator.sol"; + +/// @notice ExchangeCore contract +/// @dev contains the main functions for the marketplace +abstract contract ExchangeCore is Initializable, OwnableUpgradeable, TransferExecutor, ITransferManager { + using LibTransfer for address payable; + + /// @notice AssetMatcher contract + /// @return AssetMatcher address + IAssetMatcher public assetMatcher; + + /// @notice OrderValidator contract + /// @return OrderValidator address + IOrderValidator public orderValidator; + + uint256 private constant UINT256_MAX = type(uint256).max; + + /// @notice boolean to indicate if native tokens are accepted for meta transactions + /// @return true if native tokens are accepted for meta tx, false otherwise + bool public nativeMeta; + + /// @notice boolean to indicate if native tokens are accepted + /// @return true if native tokens are accepted, false otherwise + bool public nativeOrder; + + /// @notice stores the fills for orders + /// @return order fill + mapping(bytes32 => uint256) public fills; + + /// @notice event signaling that an order was canceled + /// @param hash order hash + event Cancel(bytes32 indexed hash); + + /// @notice event when orders match + /// @param from _msgSender + /// @param leftHash left order hash + /// @param rightHash right order hash + /// @param newLeftFill fill for left order + /// @param newRightFill fill for right order + event Match(address indexed from, bytes32 leftHash, bytes32 rightHash, uint256 newLeftFill, uint256 newRightFill); + event AssetMatcherSetted(address indexed contractAddress); + event OrderValidatorSetted(address indexed contractAddress); + event NativeUpdated(bool nativeOrder, bool metaNative); + + /// @notice initializer for ExchangeCore + /// @param newNativeOrder for orders with native token + /// @param newMetaNative for meta orders with native token + /// @dev initialize permissions for native token exchange + function __ExchangeCoreInitialize( + bool newNativeOrder, + bool newMetaNative, + address newOrderValidatorAdress + ) internal { + nativeMeta = newMetaNative; + nativeOrder = newNativeOrder; + IOrderValidator _orderValidator = IOrderValidator(newOrderValidatorAdress); + orderValidator = _orderValidator; + } + + /// @notice set AssetMatcher address + /// @param contractAddress new AssetMatcher contract address + function setAssetMatcherContract(address contractAddress) external onlyOwner { + IAssetMatcher _assetMatcher = IAssetMatcher(contractAddress); + assetMatcher = _assetMatcher; + + emit AssetMatcherSetted(contractAddress); + } + + /// @notice set OrderValidator address + /// @param contractAddress new OrderValidator contract address + function setOrderValidatorContract(address contractAddress) external onlyOwner { + IOrderValidator _orderValidator = IOrderValidator(contractAddress); + orderValidator = _orderValidator; + + emit OrderValidatorSetted(contractAddress); + } + + /// @notice get order hashKey + /// @param order to generate the hashkey + /// @dev this function is a helper for the backend + /// @return hash of order + function getHashKey(LibOrder.Order memory order) external pure returns (bytes32) { + return LibOrder.hashKey(order); + } + + /// @notice update permissions for native orders + /// @param newNativeOrder for orders with native token + /// @param newMetaNative for meta orders with native token + /// @dev setter for permissions for native token exchange + function updateNative(bool newNativeOrder, bool newMetaNative) external onlyOwner { + nativeMeta = newMetaNative; + nativeOrder = newNativeOrder; + + emit NativeUpdated(newNativeOrder, newMetaNative); + } + + /// @notice cancel order + /// @param order to be canceled + /// @dev require msg sender to be order maker and salt different from 0 + function cancel(LibOrder.Order memory order, bytes32 orderHash) external { + require(_msgSender() == order.maker, "ExchangeCore: not maker"); + require(order.salt != 0, "ExchangeCore: 0 salt can't be used"); + bytes32 orderKeyHash = LibOrder.hashKey(order); + require(orderHash == orderKeyHash, "ExchangeCore: Invalid orderHash"); + fills[orderKeyHash] = UINT256_MAX; + emit Cancel(orderKeyHash); + } + + /// @notice direct purchase orders - can handle bulk purchases + /// @param direct array of purchase order + /// @param signature array of signed message specifying order details with the buyer + /// @dev The buyer param was added so the function is compatible with Sand approveAndCall + function directPurchase( + address buyer, + LibDirectTransfer.Purchase[] calldata direct, + bytes[] calldata signature + ) external payable { + for (uint256 i; i < direct.length; ) { + _directPurchase(buyer, direct[i], signature[i]); + unchecked { + i++; + } + } + } + + /// @notice generate sellOrder and buyOrder from parameters and call validateAndMatch() for purchase transaction + /// @param direct purchase order + function _directPurchase( + address buyer, + LibDirectTransfer.Purchase calldata direct, + bytes calldata signature + ) internal { + LibAsset.AssetType memory paymentAssetType = getPaymentAssetType(direct.paymentToken); + + LibOrder.OrderBack memory orderBack = LibOrder.OrderBack( + buyer, + direct.sellOrderMaker, + LibAsset.Asset(LibAsset.AssetType(direct.nftAssetClass, direct.nftData), direct.sellOrderNftAmount), + address(0), + LibAsset.Asset(paymentAssetType, direct.sellOrderPaymentAmount), + direct.sellOrderSalt, + direct.sellOrderStart, + direct.sellOrderEnd, + direct.sellOrderDataType, + direct.sellOrderData + ); + + require(orderValidator.isPurchaseValid(orderBack, signature), "INVALID_PURCHASE"); + + LibOrder.Order memory sellOrder = LibOrder.Order( + direct.sellOrderMaker, + LibAsset.Asset(LibAsset.AssetType(direct.nftAssetClass, direct.nftData), direct.sellOrderNftAmount), + address(0), + LibAsset.Asset(paymentAssetType, direct.sellOrderPaymentAmount), + direct.sellOrderSalt, + direct.sellOrderStart, + direct.sellOrderEnd, + direct.sellOrderDataType, + direct.sellOrderData + ); + + LibOrder.Order memory buyOrder = LibOrder.Order( + buyer, + LibAsset.Asset(paymentAssetType, direct.buyOrderPaymentAmount), + address(0), + LibAsset.Asset(LibAsset.AssetType(direct.nftAssetClass, direct.nftData), direct.buyOrderNftAmount), + 0, + 0, + 0, + getOtherOrderType(direct.sellOrderDataType), + direct.buyOrderData + ); + orderValidator.verifyERC20Whitelist(direct.paymentToken); + validateFull(sellOrder, direct.sellOrderSignature); + matchAndTransfer(sellOrder, buyOrder); + } + + /// @dev function, generate sellOrder and buyOrder from parameters and call validateAndMatch() for accept bid transaction + /// @param direct struct with parameters for accept bid operation + function directAcceptBid(LibDirectTransfer.AcceptBid calldata direct) external payable { + LibAsset.AssetType memory paymentAssetType = getPaymentAssetType(direct.paymentToken); + + LibOrder.Order memory buyOrder = LibOrder.Order( + direct.bidMaker, + LibAsset.Asset(paymentAssetType, direct.bidPaymentAmount), + address(0), + LibAsset.Asset(LibAsset.AssetType(direct.nftAssetClass, direct.nftData), direct.bidNftAmount), + direct.bidSalt, + direct.bidStart, + direct.bidEnd, + direct.bidDataType, + direct.bidData + ); + + LibOrder.Order memory sellOrder = LibOrder.Order( + address(0), + LibAsset.Asset(LibAsset.AssetType(direct.nftAssetClass, direct.nftData), direct.sellOrderNftAmount), + address(0), + LibAsset.Asset(paymentAssetType, direct.sellOrderPaymentAmount), + 0, + 0, + 0, + getOtherOrderType(direct.bidDataType), + direct.sellOrderData + ); + + validateFull(buyOrder, direct.bidSignature); + matchAndTransfer(sellOrder, buyOrder); + } + + /// @notice Match orders and transact + /// @param orderLeft left order + /// @param signatureLeft signature for the left order + /// @param orderRight right signature + /// @param signatureRight signature for the right order + /// @dev validate orders through validateOrders before matchAndTransfer + function matchOrders( + LibOrder.Order memory orderLeft, + bytes memory signatureLeft, + LibOrder.Order memory orderRight, + bytes memory signatureRight + ) external payable { + validateOrders(orderLeft, signatureLeft, orderRight, signatureRight); + matchAndTransfer(orderLeft, orderRight); + } + + /// @dev function, validate orders + /// @param orderLeft left order + /// @param signatureLeft order left signature + /// @param orderRight right order + /// @param signatureRight order right signature + function validateOrders( + LibOrder.Order memory orderLeft, + bytes memory signatureLeft, + LibOrder.Order memory orderRight, + bytes memory signatureRight + ) internal view { + validateFull(orderLeft, signatureLeft); + validateFull(orderRight, signatureRight); + if (orderLeft.taker != address(0)) { + if (orderRight.maker != address(0)) require(orderRight.maker == orderLeft.taker, "leftOrder.taker failed"); + } + if (orderRight.taker != address(0)) { + if (orderLeft.maker != address(0)) require(orderRight.taker == orderLeft.maker, "rightOrder.taker failed"); + } + } + + /// @notice matches valid orders and transfers their assets + /// @param orderLeft the left order of the match + /// @param orderRight the right order of the match + function matchAndTransfer(LibOrder.Order memory orderLeft, LibOrder.Order memory orderRight) internal { + (LibAsset.AssetType memory makeMatch, LibAsset.AssetType memory takeMatch) = matchAssets(orderLeft, orderRight); + + ( + LibOrderDataGeneric.GenericOrderData memory leftOrderData, + LibOrderDataGeneric.GenericOrderData memory rightOrderData, + LibFill.FillResult memory newFill + ) = parseOrdersSetFillEmitMatch(orderLeft, orderRight); + + (uint256 totalMakeValue, uint256 totalTakeValue) = doTransfers( + LibDeal.DealSide({ + asset: LibAsset.Asset({assetType: makeMatch, value: newFill.leftValue}), + payouts: leftOrderData.payouts, + originFees: leftOrderData.originFees, + from: orderLeft.maker + }), + LibDeal.DealSide({ + asset: LibAsset.Asset(takeMatch, newFill.rightValue), + payouts: rightOrderData.payouts, + originFees: rightOrderData.originFees, + from: orderRight.maker + }), + getDealData( + makeMatch.assetClass, + takeMatch.assetClass, + orderLeft.dataType, + orderRight.dataType, + leftOrderData, + rightOrderData + ) + ); + + uint256 takeBuyAmount = newFill.rightValue; + uint256 makeBuyAmount = newFill.leftValue; + + if (((_msgSender() != msg.sender) && !nativeMeta) || ((_msgSender() == msg.sender) && !nativeOrder)) { + require(makeMatch.assetClass != LibAsset.ETH_ASSET_CLASS, "maker cannot transfer native token"); + require(takeMatch.assetClass != LibAsset.ETH_ASSET_CLASS, "taker cannot transfer native token"); + } + if (makeMatch.assetClass == LibAsset.ETH_ASSET_CLASS) { + require(takeMatch.assetClass != LibAsset.ETH_ASSET_CLASS, "taker cannot transfer native token"); + require(makeBuyAmount >= totalMakeValue, "not enough eth"); + if (makeBuyAmount > totalMakeValue) { + payable(msg.sender).transferEth(makeBuyAmount - totalMakeValue); + } + } else if (takeMatch.assetClass == LibAsset.ETH_ASSET_CLASS) { + require(takeBuyAmount >= totalTakeValue, "not enough eth"); + if (takeBuyAmount > totalTakeValue) { + payable(msg.sender).transferEth(takeBuyAmount - totalTakeValue); + } + } + } + + /// @notice parse orders with LibOrderDataGeneric parse() to get the order data, then create a new fill with setFillEmitMatch() + /// @param orderLeft left order + /// @param orderRight right order + /// @return leftOrderData generic order data from left order + /// @return rightOrderData generic order data from right order + /// @return newFill fill result + function parseOrdersSetFillEmitMatch( + LibOrder.Order memory orderLeft, + LibOrder.Order memory orderRight + ) + internal + returns ( + LibOrderDataGeneric.GenericOrderData memory leftOrderData, + LibOrderDataGeneric.GenericOrderData memory rightOrderData, + LibFill.FillResult memory newFill + ) + { + bytes32 leftOrderKeyHash = LibOrder.hashKey(orderLeft); + bytes32 rightOrderKeyHash = LibOrder.hashKey(orderRight); + + address msgSender = _msgSender(); + if (orderLeft.maker == address(0)) { + orderLeft.maker = msgSender; + } + if (orderRight.maker == address(0)) { + orderRight.maker = msgSender; + } + + leftOrderData = LibOrderDataGeneric.parse(orderLeft); + rightOrderData = LibOrderDataGeneric.parse(orderRight); + + newFill = setFillEmitMatch( + orderLeft, + orderRight, + leftOrderKeyHash, + rightOrderKeyHash, + leftOrderData.isMakeFill, + rightOrderData.isMakeFill + ); + } + + /// @notice return the deal data from orders + /// @param makeMatchAssetClass, class of make asset + /// @param takeMatchAssetClass, class of take asset + /// @param leftDataType data type of left order + /// @param rightDataType data type of right order + /// @param leftOrderData data of left order + /// @param rightOrderData data of right order + /// @dev return deal data (feeSide and maxFeesBasePoint) from orders + function getDealData( + bytes4 makeMatchAssetClass, + bytes4 takeMatchAssetClass, + bytes4 leftDataType, + bytes4 rightDataType, + LibOrderDataGeneric.GenericOrderData memory leftOrderData, + LibOrderDataGeneric.GenericOrderData memory rightOrderData + ) internal pure returns (LibDeal.DealData memory dealData) { + dealData.feeSide = LibFeeSide.getFeeSide(makeMatchAssetClass, takeMatchAssetClass); + dealData.maxFeesBasePoint = getMaxFee( + leftDataType, + rightDataType, + leftOrderData, + rightOrderData, + dealData.feeSide + ); + } + + /// @notice determines the max amount of fees for the match + /// @param dataTypeLeft data type of the left order + /// @param dataTypeRight data type of the right order + /// @param leftOrderData data of the left order + /// @param rightOrderData data of the right order + /// @param feeSide fee side of the match + /// @return max fee amount in base points + function getMaxFee( + bytes4 dataTypeLeft, + bytes4 dataTypeRight, + LibOrderDataGeneric.GenericOrderData memory leftOrderData, + LibOrderDataGeneric.GenericOrderData memory rightOrderData, + LibFeeSide.FeeSide feeSide + ) internal pure returns (uint256) { + if ( + dataTypeLeft != LibOrderData.SELL && + dataTypeRight != LibOrderData.SELL && + dataTypeLeft != LibOrderData.BUY && + dataTypeRight != LibOrderData.BUY + ) { + return 0; + } + + uint256 matchFees = getSumFees(leftOrderData.originFees, rightOrderData.originFees); + uint256 maxFee; + if (feeSide == LibFeeSide.FeeSide.LEFT) { + maxFee = rightOrderData.maxFeesBasePoint; + require(dataTypeLeft == LibOrderData.BUY && dataTypeRight == LibOrderData.SELL, "wrong V3 type1"); + } else if (feeSide == LibFeeSide.FeeSide.RIGHT) { + maxFee = leftOrderData.maxFeesBasePoint; + require(dataTypeRight == LibOrderData.BUY && dataTypeLeft == LibOrderData.SELL, "wrong V3 type2"); + } else { + return 0; + } + require(maxFee > 0 && maxFee >= matchFees && maxFee <= 1000, "wrong maxFee"); + + return maxFee; + } + + /// @notice calculates amount of fees for the match + /// @param originLeft origin fees of the left order + /// @param originRight origin fees of the right order + /// @return sum of all fees for the match (protcolFee + leftOrder.originFees + rightOrder.originFees) + function getSumFees( + LibPart.Part[] memory originLeft, + LibPart.Part[] memory originRight + ) internal pure returns (uint256) { + uint256 result = 0; + + //adding left origin fees + for (uint256 i; i < originLeft.length; i++) { + result = result + originLeft[i].value; + } + + //adding right origin fees + for (uint256 i; i < originRight.length; i++) { + result = result + originRight[i].value; + } + + return result; + } + + /// @notice calculates fills for the matched orders and set them in "fills" mapping + /// @param orderLeft left order of the match + /// @param orderRight right order of the match + /// @param leftMakeFill true if the left orders uses make-side fills, false otherwise + /// @param rightMakeFill true if the right orders uses make-side fills, false otherwise + /// @return returns change in orders' fills by the match + function setFillEmitMatch( + LibOrder.Order memory orderLeft, + LibOrder.Order memory orderRight, + bytes32 leftOrderKeyHash, + bytes32 rightOrderKeyHash, + bool leftMakeFill, + bool rightMakeFill + ) internal returns (LibFill.FillResult memory) { + uint256 leftOrderFill = getOrderFill(orderLeft.salt, leftOrderKeyHash); + uint256 rightOrderFill = getOrderFill(orderRight.salt, rightOrderKeyHash); + LibFill.FillResult memory newFill = LibFill.fillOrder( + orderLeft, + orderRight, + leftOrderFill, + rightOrderFill, + leftMakeFill, + rightMakeFill + ); + + require(newFill.rightValue > 0 && newFill.leftValue > 0, "nothing to fill"); + + if (orderLeft.salt != 0) { + if (leftMakeFill) { + fills[leftOrderKeyHash] = leftOrderFill + newFill.leftValue; + } else { + fills[leftOrderKeyHash] = leftOrderFill + newFill.rightValue; + } + } + + if (orderRight.salt != 0) { + if (rightMakeFill) { + fills[rightOrderKeyHash] = rightOrderFill + newFill.rightValue; + } else { + fills[rightOrderKeyHash] = rightOrderFill + newFill.leftValue; + } + } + + emit Match(_msgSender(), leftOrderKeyHash, rightOrderKeyHash, newFill.rightValue, newFill.leftValue); + + return newFill; + } + + /// @notice return fill corresponding to order hash + /// @param salt if salt 0, fill = 0 + /// @param hash order hash + function getOrderFill(uint256 salt, bytes32 hash) internal view returns (uint256 fill) { + if (salt == 0) { + fill = 0; + } else { + fill = fills[hash]; + } + } + + /// @notice match assets from orders + /// @param orderLeft left order + /// @param orderRight right order + /// @dev each make asset must correrspond to the other take asset and be different from 0 + function matchAssets( + LibOrder.Order memory orderLeft, + LibOrder.Order memory orderRight + ) internal view returns (LibAsset.AssetType memory makeMatch, LibAsset.AssetType memory takeMatch) { + makeMatch = assetMatcher.matchAssets(orderLeft.makeAsset.assetType, orderRight.takeAsset.assetType); + require(makeMatch.assetClass != 0, "assets don't match"); + takeMatch = assetMatcher.matchAssets(orderLeft.takeAsset.assetType, orderRight.makeAsset.assetType); + require(takeMatch.assetClass != 0, "assets don't match"); + } + + /// @notice full validation of an order + /// @param order LibOrder.Order + /// @param signature order signature + /// @dev first validate time if order start and end are within the block timestamp + /// @dev validate signature if maker is different from sender + function validateFull(LibOrder.Order memory order, bytes memory signature) internal view { + LibOrder.validateOrderTime(order); + orderValidator.validate(order, signature, _msgSender()); + } + + /// @notice return the AssetType from the token contract + /// @param token contract address + function getPaymentAssetType(address token) internal pure returns (LibAsset.AssetType memory) { + LibAsset.AssetType memory result = LibAsset.AssetType(bytes4(0), new bytes(0)); + if (token == address(0)) { + result.assetClass = LibAsset.ETH_ASSET_CLASS; + } else { + result.assetClass = LibAsset.ERC20_ASSET_CLASS; + result.data = abi.encode(token); + } + return result; + } + + /// @notice get the other order type + /// @param dataType of order + /// @dev if SELL returns BUY else if BUY returns SELL + function getOtherOrderType(bytes4 dataType) internal pure returns (bytes4) { + if (dataType == LibOrderData.SELL) { + return LibOrderData.BUY; + } + if (dataType == LibOrderData.BUY) { + return LibOrderData.SELL; + } + return dataType; + } + + uint256[49] private __gap; +} diff --git a/packages/marketplace/src/exchange/ExchangeMeta.sol b/packages/marketplace/src/exchange/ExchangeMeta.sol new file mode 100644 index 0000000000..3a45f25d8e --- /dev/null +++ b/packages/marketplace/src/exchange/ExchangeMeta.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ExchangeCore} from "./ExchangeCore.sol"; +import {ERC2771HandlerUpgradeable, ERC2771HandlerAbstract} from "@sandbox-smart-contracts/dependency-metatx/contracts/ERC2771HandlerUpgradeable.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {TransferManager, IRoyaltiesProvider} from "../transfer-manager/TransferManager.sol"; + +/// @title Exchange contract with meta transactions +/// @notice Used to exchange assets, that is, tokens. +/// @dev Main functions are in ExchangeCore +/// @dev TransferManager is used to execute token transfers +contract ExchangeMeta is ExchangeCore, TransferManager, ERC2771HandlerUpgradeable { + /// @notice ExchangeMeta contract initializer + /// @param newTrustedForwarder address for trusted forwarder that will execute meta transactions + /// @param newProtocolFeePrimary protocol fee applied for primary markets + /// @param newProtocolFeeSecondary protocol fee applied for secondary markets + /// @param newDefaultFeeReceiver market fee receiver + /// @param newRoyaltiesProvider registry for the different types of royalties + /// @param orderValidatorAdress address of the OrderValidator contract, that validates orders + /// @param newNativeOrder bool to indicate of the contract accepts or doesn't native tokens, i.e. ETH or Matic + /// @param newMetaNative same as =nativeOrder but for metaTransactions + function __Exchange_init( + address newTrustedForwarder, + uint256 newProtocolFeePrimary, + uint256 newProtocolFeeSecondary, + address newDefaultFeeReceiver, + IRoyaltiesProvider newRoyaltiesProvider, + address orderValidatorAdress, + bool newNativeOrder, + bool newMetaNative + ) external initializer { + __ERC2771Handler_init(newTrustedForwarder); + __Ownable_init(); + __TransferManager_init_unchained( + newProtocolFeePrimary, + newProtocolFeeSecondary, + newDefaultFeeReceiver, + newRoyaltiesProvider + ); + __ExchangeCoreInitialize(newNativeOrder, newMetaNative, orderValidatorAdress); + } + + function _msgSender() internal view virtual override(ContextUpgradeable, ERC2771HandlerAbstract) returns (address) { + return ERC2771HandlerAbstract._msgSender(); + } + + function _msgData() internal view override(ContextUpgradeable, ERC2771HandlerAbstract) returns (bytes calldata) { + return ERC2771HandlerAbstract._msgData(); + } + + /// @notice Change the address of the trusted forwarder for meta-transactions + /// @param newTrustedForwarder The new trustedForwarder + function setTrustedForwarder(address newTrustedForwarder) public virtual onlyOwner { + require(newTrustedForwarder != address(0), "address must be different from 0"); + + _setTrustedForwarder(newTrustedForwarder); + } +} diff --git a/packages/marketplace/src/exchange/Migrations.sol b/packages/marketplace/src/exchange/Migrations.sol new file mode 100644 index 0000000000..65ec014306 --- /dev/null +++ b/packages/marketplace/src/exchange/Migrations.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +contract Migrations { + address public owner = msg.sender; + uint256 public lastCompletedMigration; + + modifier restricted() { + require(msg.sender == owner, "This function is restricted to the contract's owner"); + _; + } + + function setCompleted(uint256 completed) public restricted { + lastCompletedMigration = completed; + } +} diff --git a/packages/marketplace/src/exchange/OrderValidator.md b/packages/marketplace/src/exchange/OrderValidator.md new file mode 100644 index 0000000000..b191a5c75e --- /dev/null +++ b/packages/marketplace/src/exchange/OrderValidator.md @@ -0,0 +1,13 @@ +#### Features + +OrderValidator is [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) Order signature validator. It also supports [EIP-1271](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1271.md) signature validation. + +##### Algorithm + +If msg sender is `order.maker`, then order validation is not needed. + +Otherwise, `Order` hash is calculated using [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md). Types for calculating hashes can be found [here](./LibOrder.md). + +If `order.maker` is a contract, then signature is checked using [EIP-1271](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1271.md) (calling `isValidSignature` function). If contract returns any value not equal to MAGICVALUE(0x1626ba7e) the function reverts. + +Otherwise signer is recovered using ECDSA. If signer is not `Order.maker`, then function reverts. diff --git a/packages/marketplace/src/exchange/OrderValidator.sol b/packages/marketplace/src/exchange/OrderValidator.sol new file mode 100644 index 0000000000..ac3e58ed3a --- /dev/null +++ b/packages/marketplace/src/exchange/OrderValidator.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibOrder, LibAsset} from "../lib-order/LibOrder.sol"; +import {IERC1271Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC1271Upgradeable.sol"; +import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import {AddressUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import {EIP712Upgradeable, Initializable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import {IOrderValidator} from "../interfaces/IOrderValidator.sol"; +import {WhiteList} from "./WhiteList.sol"; + +/// @title contract for order validation +/// @notice validate orders and contains a white list of tokens +contract OrderValidator is IOrderValidator, Initializable, EIP712Upgradeable, WhiteList { + using ECDSAUpgradeable for bytes32; + using AddressUpgradeable for address; + + address private _signingWallet; + + bytes4 internal constant MAGICVALUE = 0x1626ba7e; + + /// @notice event for when a new _signingWallet is set + /// @param newSigningWallet The new address of the signing wallet + event SigningWallet(address indexed newSigningWallet); + + /// @notice initializer for OrderValidator + /// @param newTsbOnly boolean to indicate that only The Sandbox tokens are accepted by the exchange contract + /// @param newPartners boolena to indicate that partner tokens are accepted by the exchange contract + /// @param newOpen boolean to indicate that all assets are accepted by the exchange contract + /// @param newErc20 boolean to activate the white list of ERC20 tokens + function __OrderValidator_init_unchained( + bool newTsbOnly, + bool newPartners, + bool newOpen, + bool newErc20 + ) public initializer { + __EIP712_init_unchained("Exchange", "1"); + __Whitelist_init(newTsbOnly, newPartners, newOpen, newErc20); + } + + /// @notice Update the signing wallet address + /// @param newSigningWallet The new address of the signing wallet + function setSigningWallet(address newSigningWallet) external onlyOwner { + require(newSigningWallet != address(0), "WALLET_ZERO_ADDRESS"); + require(newSigningWallet != _signingWallet, "WALLET_ALREADY_SET"); + _signingWallet = newSigningWallet; + + emit SigningWallet(newSigningWallet); + } + + /// @notice Get the wallet authorized for signing purchase-messages. + /// @return _signingWallet the address of the signing wallet + function getSigningWallet() external view returns (address) { + return _signingWallet; + } + + /// @notice verifies if backend signature is valid + /// @param orderBack order to be validated + /// @param signature signature of order + /// @return boolean comparison between the recover signature and signing wallet + function isPurchaseValid(LibOrder.OrderBack memory orderBack, bytes memory signature) external view returns (bool) { + bytes32 hash = LibOrder.backendHash(orderBack); + address recoveredSigner = _hashTypedDataV4(hash).recover(signature); + + return recoveredSigner == _signingWallet; + } + + /// @notice verifies order + /// @param order order to be validated + /// @param signature signature of order + /// @param sender order sender + function validate(LibOrder.Order memory order, bytes memory signature, address sender) public view { + if (order.makeAsset.assetType.assetClass != LibAsset.ETH_ASSET_CLASS) { + address makeToken = abi.decode(order.makeAsset.assetType.data, (address)); + verifyWhiteList(makeToken); + } + + if (order.salt == 0) { + if (order.maker != address(0)) { + require(sender == order.maker, "maker is not tx sender"); + } + } else { + if (sender != order.maker) { + bytes32 hash = LibOrder.hash(order); + // if maker is contract checking ERC1271 signature + if (order.maker.isContract()) { + require( + IERC1271Upgradeable(order.maker).isValidSignature(_hashTypedDataV4(hash), signature) == + MAGICVALUE, + "contract order signature verification error" + ); + } else { + // if maker is not contract then checking ECDSA signature + if (_hashTypedDataV4(hash).recover(signature) != order.maker) { + revert("order signature verification error"); + } else { + require(order.maker != address(0), "no maker"); + } + } + } + } + } + + /// @notice if ERC20 token is accepted + /// @param tokenAddress ERC20 token address + function verifyERC20Whitelist(address tokenAddress) external view { + if (erc20List && !erc20WhiteList[tokenAddress]) { + revert("payment token not allowed"); + } + } + + function verifyWhiteList(address tokenAddress) internal view { + if (open) { + return; + } else if ((tsbOnly && tsbWhiteList[tokenAddress]) || (partners && partnerWhiteList[tokenAddress])) { + return; + } else { + revert("not allowed"); + } + } + + uint256[50] private __gap; +} diff --git a/packages/marketplace/src/exchange/WhiteList.sol b/packages/marketplace/src/exchange/WhiteList.sol new file mode 100644 index 0000000000..f3e273411e --- /dev/null +++ b/packages/marketplace/src/exchange/WhiteList.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {IWhiteList} from "../interfaces/IWhiteList.sol"; + +/// @title WhiteList contract +/// @dev controls which tokens are accepted in the marketplace +contract WhiteList is IWhiteList, OwnableUpgradeable { + /// @notice if status == tsbOnly, then only tsbListedContracts [small mapping] + /// @return tsbOnly + bool public tsbOnly; + + /// @notice if status == partners, then tsbListedContracts and partnerContracts [manageable mapping] + /// @return partners + bool public partners; + + /// @notice if status == open, then no whitelist [no mapping needed]. But then we need a removeListing function for contracts we subsequently + /// @return open + bool public open; + + /// @notice if status == erc20List, users can only pay white whitelisted ERC20 tokens + /// @return erc20List + bool public erc20List; + + /// @notice mapping containing the list of contracts in the tsb white list + /// @return true if list contains address + mapping(address => bool) public tsbWhiteList; + + /// @notice mapping containing the list of contracts in the partners white list + /// @return true if list contains address + mapping(address => bool) public partnerWhiteList; + + /// @notice mapping containing the list of contracts in the erc20 white list + /// @return true if list contains address + mapping(address => bool) public erc20WhiteList; + + /// @notice event emitted when new permissions for tokens are added + /// @param tsbOnly boolean indicating that TSB tokens are accepted + /// @param partners boolean indicating that partner tokens are accepted + /// @param open boolean indicating that all tokens are accepted + /// @param erc20List boolean indicating that there is a restriction for ERC20 tokens + event PermissionSetted(bool tsbOnly, bool partners, bool open, bool erc20List); + + /// @notice event emitted when a new TSB token has been added + /// @param tokenAddress address of added token + event TSBAdded(address indexed tokenAddress); + + /// @notice event emitted when a TSB token has been removed + /// @param tokenAddress address of removed token + event TSBRemoved(address indexed tokenAddress); + + /// @notice event emitted when a new partner token has been added + /// @param tokenAddress address of added token + event PartnerAdded(address indexed tokenAddress); + + /// @notice event emitted when a partner token has been removed + /// @param tokenAddress address of removed token + event PartnerRemoved(address indexed tokenAddress); + + /// @notice event emitted when a new ERC20 token has been added + /// @param tokenAddress address of added token + event ERC20Added(address indexed tokenAddress); + + /// @notice event emitted when a ERC20 token has been removed + /// @param tokenAddress address of removed token + event ERC20Removed(address indexed tokenAddress); + + /// @notice initializer for WhiteList + /// @param newTsbOnly allows orders with The Sandbox token + /// @param newPartners allows orders with partner token + /// @param newOpen allows orders with any token + /// @param newErc20List allows to pay orders with only whitelisted token + function __Whitelist_init(bool newTsbOnly, bool newPartners, bool newOpen, bool newErc20List) internal initializer { + __Ownable_init(); + tsbOnly = newTsbOnly; + partners = newPartners; + open = newOpen; + erc20List = newErc20List; + } + + /// @notice setting permissions for tokens + /// @param newTsbOnly allows orders with The Sandbox token + /// @param newPartners allows orders with partner token + /// @param newOpen allows orders with any token + /// @param newErc20List allows to pay orders with only whitelisted token + function setPermissions(bool newTsbOnly, bool newPartners, bool newOpen, bool newErc20List) external onlyOwner { + tsbOnly = newTsbOnly; + partners = newPartners; + open = newOpen; + erc20List = newErc20List; + + emit PermissionSetted(tsbOnly, partners, open, erc20List); + } + + /// @notice add token to tsb list + /// @param tokenAddress token address + function addTSB(address tokenAddress) external onlyOwner { + tsbWhiteList[tokenAddress] = true; + + emit TSBAdded(tokenAddress); + } + + /// @notice remove token from tsb list + /// @param tokenAddress token address + function removeTSB(address tokenAddress) external onlyOwner { + require(tsbWhiteList[tokenAddress], "not allowed"); + tsbWhiteList[tokenAddress] = false; + + emit TSBRemoved(tokenAddress); + } + + /// @notice add token to partners list + /// @param tokenAddress token address + function addPartner(address tokenAddress) external onlyOwner { + partnerWhiteList[tokenAddress] = true; + + emit PartnerAdded(tokenAddress); + } + + /// @notice remove token from partner list + /// @param tokenAddress token address + function removePartner(address tokenAddress) external onlyOwner { + require(partnerWhiteList[tokenAddress], "not allowed"); + partnerWhiteList[tokenAddress] = false; + + emit PartnerRemoved(tokenAddress); + } + + /// @notice add token to the ERC20 list + /// @param tokenAddress token address + function addERC20(address tokenAddress) external onlyOwner { + erc20WhiteList[tokenAddress] = true; + + emit ERC20Added(tokenAddress); + } + + /// @notice remove token from ERC20 list + /// @param tokenAddress token address + function removeERC20(address tokenAddress) external onlyOwner { + require(erc20WhiteList[tokenAddress], "not allowed"); + erc20WhiteList[tokenAddress] = false; + + emit ERC20Removed(tokenAddress); + } +} diff --git a/packages/marketplace/src/exchange/documents/diagram-13983673345763902680.png b/packages/marketplace/src/exchange/documents/diagram-13983673345763902680.png new file mode 100644 index 0000000000..43a7270655 Binary files /dev/null and b/packages/marketplace/src/exchange/documents/diagram-13983673345763902680.png differ diff --git a/packages/marketplace/src/exchange/libraries/LibDirectTransfer.sol b/packages/marketplace/src/exchange/libraries/LibDirectTransfer.sol new file mode 100644 index 0000000000..da9eeabb4a --- /dev/null +++ b/packages/marketplace/src/exchange/libraries/LibDirectTransfer.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title library containing the structs for direct purchase +library LibDirectTransfer { + /// @notice Purchase, all buy parameters need for create buyOrder and sellOrder + struct Purchase { + address sellOrderMaker; + uint256 sellOrderNftAmount; + bytes4 nftAssetClass; + bytes nftData; + uint256 sellOrderPaymentAmount; + address paymentToken; + uint256 sellOrderSalt; + uint256 sellOrderStart; + uint256 sellOrderEnd; + bytes4 sellOrderDataType; + bytes sellOrderData; + bytes sellOrderSignature; + uint256 buyOrderPaymentAmount; + uint256 buyOrderNftAmount; + bytes buyOrderData; + } + + /// @notice AcceptBid, all accept bid parameters need for create buyOrder and sellOrder + struct AcceptBid { + address bidMaker; + uint256 bidNftAmount; + bytes4 nftAssetClass; + bytes nftData; + uint256 bidPaymentAmount; + address paymentToken; + uint256 bidSalt; + uint256 bidStart; + uint256 bidEnd; + bytes4 bidDataType; + bytes bidData; + bytes bidSignature; + uint256 sellOrderPaymentAmount; + uint256 sellOrderNftAmount; + bytes sellOrderData; + } +} diff --git a/packages/marketplace/src/exchange/libraries/LibFill.md b/packages/marketplace/src/exchange/libraries/LibFill.md new file mode 100644 index 0000000000..8687583d2e --- /dev/null +++ b/packages/marketplace/src/exchange/libraries/LibFill.md @@ -0,0 +1,18 @@ +#### Features + +This library provides `fillOrder` function. It calculates fill of both orders (part of the Order that can be filled). + +It takes these parameters: +- `Order` leftOrder - left order (sent to matchOrders) +- `Order` rightOrder - right order (sent to matchOrders) +- `uint` leftOrderFill - previous fill for the left `Order` (zero if it's not filled) +- `uint` rightOrderFill - previous fill for the right `Order` (zero if it's not filled) + +If orders are fully filled, then left order's make value is equal to right's order take value and left order's take value is equal to right order's make value. + +There are 3 cases to calculate new fills: +1. Left order's take value > right order's make value +2. Right order's take value > left order's make value +3. Otherwise + +See tests [here](../../exchange-v2/test/v2/LibFill.test.js) for all possible variants \ No newline at end of file diff --git a/packages/marketplace/src/exchange/libraries/LibFill.sol b/packages/marketplace/src/exchange/libraries/LibFill.sol new file mode 100644 index 0000000000..dcae893d94 --- /dev/null +++ b/packages/marketplace/src/exchange/libraries/LibFill.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {LibOrder, LibMath} from "../../lib-order/LibOrder.sol"; + +/// @title This library provides `fillOrder` function. +/// @notice It calculates fill of both orders (part of the Order that can be filled). +library LibFill { + struct FillResult { + uint256 leftValue; + uint256 rightValue; + } + + struct IsMakeFill { + bool leftMake; + bool rightMake; + } + + /// @notice Should return filled values + /// @param leftOrder left order + /// @param rightOrder right order + /// @param leftOrderFill current fill of the left order (0 if order is unfilled) + /// @param rightOrderFill current fill of the right order (0 if order is unfilled) + /// @param leftIsMakeFill true if left orders fill is calculated from the make side, false if from the take side + /// @param rightIsMakeFill true if right orders fill is calculated from the make side, false if from the take side + /// @dev We have 3 cases, 1st: left order should be fully filled + /// @dev 2nd: right order should be fully filled or 3d: both should be fully filled if required values are the same + /// @return the fill result of both orders + function fillOrder( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + uint256 leftOrderFill, + uint256 rightOrderFill, + bool leftIsMakeFill, + bool rightIsMakeFill + ) internal pure returns (FillResult memory) { + (uint256 leftMakeValue, uint256 leftTakeValue) = LibOrder.calculateRemaining( + leftOrder, + leftOrderFill, + leftIsMakeFill + ); + (uint256 rightMakeValue, uint256 rightTakeValue) = LibOrder.calculateRemaining( + rightOrder, + rightOrderFill, + rightIsMakeFill + ); + + if (rightTakeValue > leftMakeValue) { + return fillLeft(leftMakeValue, leftTakeValue, rightOrder.makeAsset.value, rightOrder.takeAsset.value); + } + return fillRight(leftOrder.makeAsset.value, leftOrder.takeAsset.value, rightMakeValue, rightTakeValue); + } + + function fillRight( + uint256 leftMakeValue, + uint256 leftTakeValue, + uint256 rightMakeValue, + uint256 rightTakeValue + ) internal pure returns (FillResult memory result) { + uint256 makerValue = LibMath.safeGetPartialAmountFloor(rightTakeValue, leftMakeValue, leftTakeValue); + require(makerValue <= rightMakeValue, "fillRight: unable to fill"); + return FillResult(rightTakeValue, makerValue); + } + + function fillLeft( + uint256 leftMakeValue, + uint256 leftTakeValue, + uint256 rightMakeValue, + uint256 rightTakeValue + ) internal pure returns (FillResult memory result) { + uint256 rightTake = LibMath.safeGetPartialAmountFloor(leftTakeValue, rightMakeValue, rightTakeValue); + require(rightTake <= leftMakeValue, "fillLeft: unable to fill"); + return FillResult(leftMakeValue, leftTakeValue); + } +} diff --git a/packages/marketplace/src/exchange/libraries/LibOrder.md b/packages/marketplace/src/exchange/libraries/LibOrder.md new file mode 100644 index 0000000000..ef76ad8345 --- /dev/null +++ b/packages/marketplace/src/exchange/libraries/LibOrder.md @@ -0,0 +1,43 @@ +#### Features + +This library contains struct `Order` with some functions for this struct: +- hash: calculates hash according to EIP-712 rules. you can find type definitions +- hashKey: calculates key for Order used to record fill of the order (orders with the same key considered as an update) +- validate: validates main order parameters, checks if `Order` can be processed +- calculateRemaining: calculates remaining part of the `Order` (if it's partially filled) + +`Order` fields: +- `address` maker +- `Asset` leftAsset (see [LibAsset](../../lib-asset/LibAsset.md)) +- `address` taker (can be zero address) +- `Asset` rightAsset (see [LibAsset](../../lib-asset/LibAsset.md)) +- `uint` salt - random number to distinguish different maker's Orders +- `uint` start - Order can't be matched before this date (optional) +- `uint` end - Order can't be matched after this date (optional) +- `bytes4` dataType - type of data, usually hash of some string, e.g.: "v1", "v2" (see more [here](LibOrderData.md)) +- `bytes` data - generic data, can be anything, extendable part of the order (see more [here](LibOrderData.md)) + +#### Types for EIP-712 signature: +```javascript +const Types = { + AssetType: [ + {name: 'assetClass', type: 'bytes4'}, + {name: 'data', type: 'bytes'} + ], + Asset: [ + {name: 'assetType', type: 'AssetType'}, + {name: 'value', type: 'uint256'} + ], + Order: [ + {name: 'maker', type: 'address'}, + {name: 'makeAsset', type: 'Asset'}, + {name: 'taker', type: 'address'}, + {name: 'takeAsset', type: 'Asset'}, + {name: 'salt', type: 'uint256'}, + {name: 'start', type: 'uint256'}, + {name: 'end', type: 'uint256'}, + {name: 'dataType', type: 'bytes4'}, + {name: 'data', type: 'bytes'}, + ] +}; +``` diff --git a/packages/marketplace/src/exchange/libraries/LibOrderData.md b/packages/marketplace/src/exchange/libraries/LibOrderData.md new file mode 100644 index 0000000000..1e29d2d9e7 --- /dev/null +++ b/packages/marketplace/src/exchange/libraries/LibOrderData.md @@ -0,0 +1,75 @@ +# Features + +## Data types, corresponding transfers/fees logic +`Order` data can be generic. `dataType` field defines format of that data. +- `"0xffffffff"` or `"no type"` + - no data + - fees logic + - no fees +- `"V1"` + - fields + - `LibPart.Part[] payouts` + - array of payouts, i.e. how takeAsset of the order is going to be distributed. (usually 100% goes to order.maker, can be something like 50% goes to maker, 50% to someone else. it can be divided in any other way) + - `LibPart.Part[] originFees` + - additional fees (e.g. 5% of the payment goes to additional address) + - fees logic + - `originFees` from buy-order is taken from the buyer, `originFees` from sell-order is taken from the seller. e.g. sell order is `1 ERC721` => `100 ETH`, buy order is `100 ETH` => `1 ERC721`. Buy order has `originFees` = [`{5% to addr1}`,`{10% to addr2}`]. Sell order has `originFees` = [`{5% to addr3}`]. Then, total amount that buyer needs to send is `100 ETH` + `5%*100ETH` + `10%*100ETH` = `115 ETH` (15% more than order value, buy-order `origin fees` are added. So buyer pays for their `origin fees`). From this amount `5 ETH` will be transferred to addr1, `10 ETH` to addr2 (now we have `100ETH` left) and `5ETH` to addr3 (it is seller `origin fee`, so it is taken from their part). + - after that NFT `royalties` are taken + - what's left after that is distributed according to sell-order `payouts` +- `"V2"` + - fields + - `LibPart.Part[] payouts`, works the same as in `V1` orders + - `LibPart.Part[] originFees`, works the same as in `V1` orders + - `bool isMakeFill` + - if false, order's `fill` (what part of order is completed, stored on-chain) is calculated from take side (in `V1` orders it always works like that) + - if true, `fill` is calculated from the make side of the order + - fees logic, works the same as in `V1` orders +- `"V3"` two types of `V3` orders. + - `"V3_BUY"` + - fields + - `uint payouts`, works the same as in `V1` orders, but there is only 1 value and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeFirst`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeSecond`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `bytes32 marketplaceMarker`, bytes32 id marketplace, which generate this order + - `"V3_SELL"` + - fields + - `uint payouts`, works the same as in `V1` orders, but there is only 1 value and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeFirst`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeSecond`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint maxFeesBasePoint` + - maximum amount of fees that can be taken from payment (e.g. 10%) + - chosen by seller, that's why it's only present in `V3_SELL` orders + - `maxFeesBasePoint` should be more than `0` + - `maxFeesBasePoint` should not be bigger than `10%` + - `bytes32 marketplaceMarker`, bytes32 id marketplace, which generate this order + - `V3` orders can only be matched if buy-order is `V3_BUY` and the sell-order is `V3_SELL` + - `V3` orders don't have `isMakeFill` field + - `V3_SELL` orders' fills are always calculated from make side (as if `isMakeFill` = true) + - `V3_BUY` orders' fills are always calculated from take side (as if `isMakeFill` = false) + - fees logic + - `V3` orders' fees work differently from all previous orders types + - `originFees` are taken from seller side only. + - sum of buy-order `originFees` + sell-order `originFees` should not be bigger than `maxFeesBasePoint` + - example: + - sell order is `1 ERC721` => `100 ETH` + - `maxFeesBasePoint` is 10 % + - Sell order has `originFeeFirst` = `{2% to addr3}` + - buy order is `100 ETH` => `1 ERC721` + - Buy order has + - `originFeeFirst` = `{3% to addr1}`, + - `originFeeSecond` = `{2% to addr2}` + - total amount for buyer is not affected by fees. it remains `100 ETH` + - `3% * 100 ETH` + `2% * 100 ETH` is taken as buy order's origin fee, `95 ETH` remaining + - `2% * 100 ETH` is taken as sell order's origin, `93 ETH` remaining + - after that NFT `royalties` are taken (same as with previous orders' types) + - what's left after that is distributed according to sell-order `payouts` (same as with previous orders' types) + + + +## Data parsing + +LibOrderData defines function parse which parses data field (according to dataType) and converts any version of the data to the GenericOrderData struct. +(see [LibOrder](LibOrder.md) `Order.data` field) + + + diff --git a/packages/marketplace/src/exchange/libraries/LibOrderDataGeneric.sol b/packages/marketplace/src/exchange/libraries/LibOrderDataGeneric.sol new file mode 100644 index 0000000000..d23f062d77 --- /dev/null +++ b/packages/marketplace/src/exchange/libraries/LibOrderDataGeneric.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibOrder} from "../../lib-order/LibOrder.sol"; +import {LibOrderData} from "../../lib-order/LibOrderData.sol"; +import {LibPart} from "../../lib-part/LibPart.sol"; + +library LibOrderDataGeneric { + struct GenericOrderData { + LibPart.Part[] payouts; + LibPart.Part[] originFees; + bool isMakeFill; + uint256 maxFeesBasePoint; + } + + function parse(LibOrder.Order memory order) internal pure returns (GenericOrderData memory dataOrder) { + if (order.dataType == LibOrderData.SELL) { + LibOrderData.DataSell memory data = abi.decode(order.data, (LibOrderData.DataSell)); + dataOrder.payouts = parsePayouts(data.payouts); + dataOrder.originFees = parseOriginFeeData(data.originFeeFirst, data.originFeeSecond); + dataOrder.isMakeFill = true; + dataOrder.maxFeesBasePoint = data.maxFeesBasePoint; + } else if (order.dataType == LibOrderData.BUY) { + LibOrderData.DataBuy memory data = abi.decode(order.data, (LibOrderData.DataBuy)); + dataOrder.payouts = parsePayouts(data.payouts); + dataOrder.originFees = parseOriginFeeData(data.originFeeFirst, data.originFeeSecond); + dataOrder.isMakeFill = false; + // solhint-disable-next-line no-empty-blocks + } else if (order.dataType == 0xffffffff) {} else { + revert("Unknown Order data type"); + } + if (dataOrder.payouts.length == 0) { + dataOrder.payouts = payoutSet(order.maker); + } + } + + function payoutSet(address orderAddress) internal pure returns (LibPart.Part[] memory) { + LibPart.Part[] memory payout = new LibPart.Part[](1); + payout[0].account = payable(orderAddress); + payout[0].value = 10000; + return payout; + } + + function parseOriginFeeData(uint256 dataFirst, uint256 dataSecond) internal pure returns (LibPart.Part[] memory) { + LibPart.Part[] memory originFee; + + if (dataFirst > 0 && dataSecond > 0) { + originFee = new LibPart.Part[](2); + + originFee[0] = uintToLibPart(dataFirst); + originFee[1] = uintToLibPart(dataSecond); + } + + if (dataFirst > 0 && dataSecond == 0) { + originFee = new LibPart.Part[](1); + + originFee[0] = uintToLibPart(dataFirst); + } + + if (dataFirst == 0 && dataSecond > 0) { + originFee = new LibPart.Part[](1); + + originFee[0] = uintToLibPart(dataSecond); + } + + return originFee; + } + + function parsePayouts(uint256 data) internal pure returns (LibPart.Part[] memory) { + LibPart.Part[] memory payouts; + + if (data > 0) { + payouts = new LibPart.Part[](1); + payouts[0] = uintToLibPart(data); + } + + return payouts; + } + + /** + @notice converts uint256 to LibPart.Part + @param data address and value encoded in uint256 (first 12 bytes ) + @return result LibPart.Part + */ + function uintToLibPart(uint256 data) internal pure returns (LibPart.Part memory result) { + if (data > 0) { + result.account = payable(address(uint160(data))); + result.value = uint96(data >> 160); + } + } +} diff --git a/packages/marketplace/src/exchange/mocks/ERC1155LazyMintTest.sol b/packages/marketplace/src/exchange/mocks/ERC1155LazyMintTest.sol new file mode 100644 index 0000000000..362d80debc --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/ERC1155LazyMintTest.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {ERC1155LazyMintTest} from "../../lazy-mint/mocks/ERC1155LazyMintTest.sol"; diff --git a/packages/marketplace/src/exchange/mocks/ERC721LazyMintTest.sol b/packages/marketplace/src/exchange/mocks/ERC721LazyMintTest.sol new file mode 100644 index 0000000000..1539d39cfe --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/ERC721LazyMintTest.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {ERC721LazyMintTest} from "../../lazy-mint/mocks/ERC721LazyMintTest.sol"; diff --git a/packages/marketplace/src/exchange/mocks/ExchangeSimple.sol b/packages/marketplace/src/exchange/mocks/ExchangeSimple.sol new file mode 100644 index 0000000000..774413fb7c --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/ExchangeSimple.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ExchangeCore} from "../ExchangeCore.sol"; +import {SimpleTransferManager} from "./SimpleTransferManager.sol"; +import {ERC2771HandlerUpgradeable, ERC2771HandlerAbstract} from "@sandbox-smart-contracts/dependency-metatx/contracts/ERC2771HandlerUpgradeable.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; + +contract ExchangeSimple is ExchangeCore, SimpleTransferManager, ERC2771HandlerUpgradeable { + function __ExchangeSimple_init( + address _orderValidatorAdress, + bool _nativeOrder, + bool _metaNative + ) external initializer { + __Ownable_init(); + __ExchangeCoreInitialize(_nativeOrder, _metaNative, _orderValidatorAdress); + } + + function _msgSender() internal view virtual override(ERC2771HandlerAbstract, ContextUpgradeable) returns (address) { + return ERC2771HandlerAbstract._msgSender(); + } + + function _msgData() internal view override(ERC2771HandlerAbstract, ContextUpgradeable) returns (bytes calldata) { + return ERC2771HandlerAbstract._msgData(); + } +} diff --git a/packages/marketplace/src/exchange/mocks/ExchangeSimple1.sol b/packages/marketplace/src/exchange/mocks/ExchangeSimple1.sol new file mode 100644 index 0000000000..a5a74df555 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/ExchangeSimple1.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ExchangeSimple} from "./ExchangeSimple.sol"; + +contract ExchangeSimple1 is ExchangeSimple { + function getSomething() external pure returns (uint256) { + return 10; + } +} diff --git a/packages/marketplace/src/exchange/mocks/ExchangeTestImports.sol b/packages/marketplace/src/exchange/mocks/ExchangeTestImports.sol new file mode 100644 index 0000000000..12c41d6974 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/ExchangeTestImports.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable no-unused-import +import {RoyaltiesRegistry} from "../../royalties-registry/RoyaltiesRegistry.sol"; +import {MintableERC20} from "./tokens/MintableERC20.sol"; +import {MintableERC721} from "./tokens/MintableERC721.sol"; +import {MintableERC1155} from "./tokens/MintableERC1155.sol"; +import {MintableERC721WithRoyalties} from "./tokens/MintableERC721WithRoyalties.sol"; +import {MintableERC1155WithRoyalties} from "./tokens/MintableERC1155WithRoyalties.sol"; +import {ExchangeMeta} from "../ExchangeMeta.sol"; + +// solhint-enable no-unused-import + +// solhint-disable-next-line no-empty-blocks +contract ExchangeTestImports { + +} diff --git a/packages/marketplace/src/exchange/mocks/LibFillTest.sol b/packages/marketplace/src/exchange/mocks/LibFillTest.sol new file mode 100644 index 0000000000..3db272d283 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/LibFillTest.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibOrder} from "../../lib-order/LibOrder.sol"; +import {LibFill} from "../libraries/LibFill.sol"; + +contract LibFillTest { + function fillOrder( + LibOrder.Order calldata leftOrder, + LibOrder.Order calldata rightOrder, + uint256 leftOrderFill, + uint256 rightOrderFill, + bool leftIsMakeFill, + bool rightIsMakeFill + ) external pure returns (LibFill.FillResult memory) { + return LibFill.fillOrder(leftOrder, rightOrder, leftOrderFill, rightOrderFill, leftIsMakeFill, rightIsMakeFill); + } +} diff --git a/packages/marketplace/src/exchange/mocks/LibOrderTest.sol b/packages/marketplace/src/exchange/mocks/LibOrderTest.sol new file mode 100644 index 0000000000..62c41fcfe6 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/LibOrderTest.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibOrder, LibAsset} from "../../lib-order/LibOrder.sol"; + +contract LibOrderTest { + function calculateRemaining( + LibOrder.Order calldata order, + uint256 fill, + bool isMakeFill + ) external pure returns (uint256 makeAmount, uint256 takeAmount) { + return LibOrder.calculateRemaining(order, fill, isMakeFill); + } + + function hashKey(LibOrder.Order calldata order) external pure returns (bytes32) { + return LibOrder.hashKey(order); + } + + function hashKeyOnChain(LibOrder.Order calldata order) external pure returns (bytes32) { + return LibOrder.hashKey(order); + } + + function validate(LibOrder.Order calldata order) external view { + LibOrder.validateOrderTime(order); + } + + function hashV2( + address maker, + LibAsset.Asset memory makeAsset, + LibAsset.Asset memory takeAsset, + uint256 salt, + bytes memory data + ) public pure returns (bytes32) { + return + keccak256( + abi.encode(maker, LibAsset.hash(makeAsset.assetType), LibAsset.hash(takeAsset.assetType), salt, data) + ); + } + + function hashV1( + address maker, + LibAsset.Asset memory makeAsset, + LibAsset.Asset memory takeAsset, + uint256 salt + ) public pure returns (bytes32) { + return + keccak256(abi.encode(maker, LibAsset.hash(makeAsset.assetType), LibAsset.hash(takeAsset.assetType), salt)); + } +} diff --git a/packages/marketplace/src/exchange/mocks/OrderValidatorTest.sol b/packages/marketplace/src/exchange/mocks/OrderValidatorTest.sol new file mode 100644 index 0000000000..6681b5a15b --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/OrderValidatorTest.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {OrderValidator, LibOrder} from "../OrderValidator.sol"; + +contract OrderValidatorTest is OrderValidator { + function __OrderValidatorTest_init(bool _tsbOnly, bool _partners, bool _open, bool _erc20) external initializer { + __OrderValidator_init_unchained(_tsbOnly, _partners, _open, _erc20); + } + + function validateOrderTest(LibOrder.Order calldata order, bytes calldata signature) external view { + return validate(order, signature, _msgSender()); + } +} diff --git a/packages/marketplace/src/exchange/mocks/RaribleTestHelper.sol b/packages/marketplace/src/exchange/mocks/RaribleTestHelper.sol new file mode 100644 index 0000000000..e96ceb9308 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/RaribleTestHelper.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibOrderData} from "../../lib-order/LibOrderData.sol"; +import {LibOrder} from "../../lib-order/LibOrder.sol"; +import {LibAsset} from "../../lib-asset/LibAsset.sol"; + +contract RaribleTestHelper { + function encode_SELL(LibOrderData.DataSell memory data) external pure returns (bytes memory) { + return abi.encode(data); + } + + function encode_BUY(LibOrderData.DataBuy memory data) external pure returns (bytes memory) { + return abi.encode(data); + } + + function encodeOriginFeeIntoUint(address account, uint96 value) external pure returns (uint256) { + return (uint256(value) << 160) + uint256(uint160(account)); + } + + function hashKey(LibOrder.Order calldata order) external pure returns (bytes32) { + return LibOrder.hashKey(order); + } + + function hashV2( + address maker, + LibAsset.Asset memory makeAsset, + LibAsset.Asset memory takeAsset, + uint256 salt, + bytes memory data + ) public pure returns (bytes32) { + return + keccak256( + abi.encode(maker, LibAsset.hash(makeAsset.assetType), LibAsset.hash(takeAsset.assetType), salt, data) + ); + } +} diff --git a/packages/marketplace/src/exchange/mocks/SimpleTransferManager.sol b/packages/marketplace/src/exchange/mocks/SimpleTransferManager.sol new file mode 100644 index 0000000000..e4be97c20b --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/SimpleTransferManager.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ITransferManager, LibDeal} from "../../transfer-manager/interfaces/ITransferManager.sol"; + +abstract contract SimpleTransferManager is ITransferManager { + function doTransfers( + LibDeal.DealSide memory left, + LibDeal.DealSide memory right, + LibDeal.DealData memory + ) internal override returns (uint256 totalMakeValue, uint256 totalTakeValue) { + transfer(left.asset, left.from, right.from); + transfer(right.asset, right.from, left.from); + totalMakeValue = left.asset.value; + totalTakeValue = right.asset.value; + } + + uint256[50] private __gap; +} diff --git a/packages/marketplace/src/exchange/mocks/TestAssetMatcher.sol b/packages/marketplace/src/exchange/mocks/TestAssetMatcher.sol new file mode 100644 index 0000000000..6bac8a9739 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestAssetMatcher.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IAssetMatcher, LibAsset} from "../../interfaces/IAssetMatcher.sol"; + +contract TestAssetMatcher is IAssetMatcher { + function matchAssets( + LibAsset.AssetType memory leftAssetType, + LibAsset.AssetType memory rightAssetType + ) external pure override returns (LibAsset.AssetType memory) { + if (leftAssetType.assetClass == bytes4(keccak256("BLA"))) { + address leftToken = abi.decode(leftAssetType.data, (address)); + address rightToken = abi.decode(rightAssetType.data, (address)); + if (leftToken == rightToken) { + return LibAsset.AssetType(rightAssetType.assetClass, rightAssetType.data); + } + } + return LibAsset.AssetType(0, ""); + } +} diff --git a/packages/marketplace/src/exchange/mocks/TestERC1155WithRoyaltyV2981.sol b/packages/marketplace/src/exchange/mocks/TestERC1155WithRoyaltyV2981.sol new file mode 100644 index 0000000000..ee787008ba --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestERC1155WithRoyaltyV2981.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC1155WithRoyaltyV2981} from "../../royalties-registry/mocks/tokens/TestERC1155WithRoyaltyV2981.sol"; diff --git a/packages/marketplace/src/exchange/mocks/TestERC1271.sol b/packages/marketplace/src/exchange/mocks/TestERC1271.sol new file mode 100644 index 0000000000..5aa8f47ef6 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestERC1271.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC1271Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC1271Upgradeable.sol"; + +contract TestERC1271 is IERC1271Upgradeable { + bool private returnSuccessfulValidSignature; + + /// @notice ERC1271 interface id + /// @dev this.isValidSignature.selector + /// @return the interface id + bytes4 public constant ERC1271_INTERFACE_ID = 0xfb855dc9; + + /// @notice valid id signature + /// @return return ERC1271_RETURN_VALID_SIGNATURE + bytes4 public constant ERC1271_RETURN_VALID_SIGNATURE = 0x1626ba7e; + + /// @notice invalid id signature + /// @return return ERC1271_RETURN_INVALID_SIGNATURE + bytes4 public constant ERC1271_RETURN_INVALID_SIGNATURE = 0x00000000; + + function setReturnSuccessfulValidSignature(bool value) public { + returnSuccessfulValidSignature = value; + } + + function isValidSignature(bytes32, bytes memory) public view override returns (bytes4) { + return returnSuccessfulValidSignature ? ERC1271_RETURN_VALID_SIGNATURE : ERC1271_RETURN_INVALID_SIGNATURE; + } +} diff --git a/packages/marketplace/src/exchange/mocks/TestERC20.sol b/packages/marketplace/src/exchange/mocks/TestERC20.sol new file mode 100644 index 0000000000..a9c230f8cb --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC20} from "../../test/TestERC20.sol"; diff --git a/packages/marketplace/src/exchange/mocks/TestERC721.sol b/packages/marketplace/src/exchange/mocks/TestERC721.sol new file mode 100644 index 0000000000..b310c94706 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestERC721.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC721} from "../../test/TestERC721.sol"; diff --git a/packages/marketplace/src/exchange/mocks/TestERC721WithRoyaltyV2981.sol b/packages/marketplace/src/exchange/mocks/TestERC721WithRoyaltyV2981.sol new file mode 100644 index 0000000000..bc6e2ce45e --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestERC721WithRoyaltyV2981.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC721WithRoyaltyV2981} from "../../royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981.sol"; diff --git a/packages/marketplace/src/exchange/mocks/TestERC721WithRoyaltyV2981Multi.sol b/packages/marketplace/src/exchange/mocks/TestERC721WithRoyaltyV2981Multi.sol new file mode 100644 index 0000000000..d5ada81583 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestERC721WithRoyaltyV2981Multi.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC721WithRoyaltyV2981Multi} from "../../royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981Multi.sol"; diff --git a/packages/marketplace/src/exchange/mocks/TestMinimalForwarder.sol b/packages/marketplace/src/exchange/mocks/TestMinimalForwarder.sol new file mode 100644 index 0000000000..a1e5a143b0 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestMinimalForwarder.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {MinimalForwarder} from "@openzeppelin/contracts/metatx/MinimalForwarder.sol"; + +contract TestMinimalForwarder is MinimalForwarder { + constructor() MinimalForwarder() {} + + //Expand function execute and treat error + function executeV(ForwardRequest calldata req, bytes calldata signature) public payable returns (bytes memory) { + bool success; + bytes memory returndata; + (success, returndata) = MinimalForwarder.execute(req, signature); + require(success, "meta transaction failed"); + return returndata; + } +} diff --git a/packages/marketplace/src/exchange/mocks/TestRoyaltiesRegistry.sol b/packages/marketplace/src/exchange/mocks/TestRoyaltiesRegistry.sol new file mode 100644 index 0000000000..366dc36d48 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TestRoyaltiesRegistry.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IRoyaltiesProvider} from "../../interfaces/IRoyaltiesProvider.sol"; +import {LibPart} from "../../lib-part/LibPart.sol"; +import {LibRoyalties2981} from "../../royalties/LibRoyalties2981.sol"; +import {IERC2981} from "../../royalties/IERC2981.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +contract TestRoyaltiesRegistry is IRoyaltiesProvider { + struct RoyaltiesSet { + bool initialized; + LibPart.Part[] royalties; + } + + mapping(bytes32 => RoyaltiesSet) public royaltiesByTokenAndTokenId; + mapping(address => RoyaltiesSet) public royaltiesByToken; + + function setRoyaltiesByToken(address token, LibPart.Part[] memory royalties) external { + uint256 sumRoyalties = 0; + for (uint256 i = 0; i < royalties.length; ++i) { + require(royalties[i].account != address(0x0), "RoyaltiesByToken recipient should be present"); + require(royalties[i].value != 0, "Fee value for RoyaltiesByToken should be > 0"); + royaltiesByToken[token].royalties.push(royalties[i]); + sumRoyalties += royalties[i].value; + } + require(sumRoyalties < 10000, "Set by token royalties sum more, than 100%"); + royaltiesByToken[token].initialized = true; + } + + function setRoyaltiesByTokenAndTokenId(address token, uint256 tokenId, LibPart.Part[] memory royalties) external { + setRoyaltiesCacheByTokenAndTokenId(token, tokenId, royalties); + } + + function getRoyalties(address token, uint256 tokenId) external view override returns (LibPart.Part[] memory) { + RoyaltiesSet memory royaltiesSet = royaltiesByTokenAndTokenId[keccak256(abi.encode(token, tokenId))]; + if (royaltiesSet.initialized) { + return royaltiesSet.royalties; + } + royaltiesSet = royaltiesByToken[token]; + if (royaltiesSet.initialized) { + return royaltiesSet.royalties; + } else if (IERC165Upgradeable(token).supportsInterface(LibRoyalties2981._INTERFACE_ID_ROYALTIES)) { + IERC2981 v2981 = IERC2981(token); + try v2981.royaltyInfo(tokenId, LibRoyalties2981._WEIGHT_VALUE) returns ( + address receiver, + uint256 royaltyAmount + ) { + return LibRoyalties2981.calculateRoyalties(receiver, royaltyAmount); + // solhint-disable-next-line no-empty-blocks + } catch {} + } + return royaltiesSet.royalties; + } + + function setRoyaltiesCacheByTokenAndTokenId( + address token, + uint256 tokenId, + LibPart.Part[] memory royalties + ) internal { + uint256 sumRoyalties = 0; + bytes32 key = keccak256(abi.encode(token, tokenId)); + for (uint256 i = 0; i < royalties.length; ++i) { + require(royalties[i].account != address(0x0), "RoyaltiesByTokenAndTokenId recipient should be present"); + require(royalties[i].value != 0, "Fee value for RoyaltiesByTokenAndTokenId should be > 0"); + royaltiesByTokenAndTokenId[key].royalties.push(royalties[i]); + sumRoyalties += royalties[i].value; + } + require(sumRoyalties < 10000, "Set by token and tokenId royalties sum more, than 100%"); + royaltiesByTokenAndTokenId[key].initialized = true; + } + + uint256[46] private __gap; +} diff --git a/packages/marketplace/src/exchange/mocks/TransferManagerTest.sol b/packages/marketplace/src/exchange/mocks/TransferManagerTest.sol new file mode 100644 index 0000000000..785fc64b6e --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/TransferManagerTest.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {TransferManager} from "../../transfer-manager/TransferManager.sol"; +import {TransferExecutor} from "../../transfer-manager/TransferExecutor.sol"; +import {LibDeal, LibPart, LibFeeSide} from "../../transfer-manager/lib/LibDeal.sol"; +import {IRoyaltiesProvider} from "../../interfaces/IRoyaltiesProvider.sol"; +import {LibOrderDataGeneric, LibOrderData, LibOrder} from "../libraries/LibOrderDataGeneric.sol"; + +contract TransferManagerTest is TransferManager, TransferExecutor { + struct ProtocolFeeSide { + LibFeeSide.FeeSide feeSide; + } + + function init____( + uint256 newProtocolFeePrimary, + uint256 newProtocolFeeSecondary, + address newCommunityWallet, + IRoyaltiesProvider newRoyaltiesProvider + ) external initializer { + __Ownable_init(); + __TransferManager_init_unchained( + newProtocolFeePrimary, + newProtocolFeeSecondary, + newCommunityWallet, + newRoyaltiesProvider + ); + } + + function getDealSide( + LibOrder.Order memory order, + LibOrderDataGeneric.GenericOrderData memory orderData + ) internal pure returns (LibDeal.DealSide memory dealSide) { + dealSide = LibDeal.DealSide(order.makeAsset, orderData.payouts, orderData.originFees, order.maker); + } + + function getDealData( + bytes4 makeMatchAssetClass, + bytes4 takeMatchAssetClass, + bytes4 leftDataType, + bytes4 rightDataType, + LibOrderDataGeneric.GenericOrderData memory leftOrderData, + LibOrderDataGeneric.GenericOrderData memory rightOrderData + ) internal pure returns (LibDeal.DealData memory dealData) { + dealData.feeSide = LibFeeSide.getFeeSide(makeMatchAssetClass, takeMatchAssetClass); + dealData.maxFeesBasePoint = getMaxFee( + leftDataType, + rightDataType, + leftOrderData, + rightOrderData, + dealData.feeSide + ); + } + + /** + @notice determines the max amount of fees for the match + @param dataTypeLeft data type of the left order + @param dataTypeRight data type of the right order + @param leftOrderData data of the left order + @param rightOrderData data of the right order + @param feeSide fee side of the match + @return max fee amount in base points + */ + function getMaxFee( + bytes4 dataTypeLeft, + bytes4 dataTypeRight, + LibOrderDataGeneric.GenericOrderData memory leftOrderData, + LibOrderDataGeneric.GenericOrderData memory rightOrderData, + LibFeeSide.FeeSide feeSide + ) internal pure returns (uint256) { + if ( + dataTypeLeft != LibOrderData.SELL && + dataTypeRight != LibOrderData.SELL && + dataTypeLeft != LibOrderData.BUY && + dataTypeRight != LibOrderData.BUY + ) { + return 0; + } + + uint256 matchFees = getSumFees(leftOrderData.originFees, rightOrderData.originFees); + uint256 maxFee; + if (feeSide == LibFeeSide.FeeSide.LEFT) { + maxFee = rightOrderData.maxFeesBasePoint; + require(dataTypeLeft == LibOrderData.BUY && dataTypeRight == LibOrderData.SELL, "wrong V3 type1"); + } else if (feeSide == LibFeeSide.FeeSide.RIGHT) { + maxFee = leftOrderData.maxFeesBasePoint; + require(dataTypeRight == LibOrderData.BUY && dataTypeLeft == LibOrderData.SELL, "wrong V3 type2"); + } else { + return 0; + } + require(maxFee > 0 && maxFee >= matchFees && maxFee <= 1000, "wrong maxFee"); + + return maxFee; + } + + /** + @notice calculates amount of fees for the match + @param originLeft origin fees of the left order + @param originRight origin fees of the right order + @return sum of all fees for the match (protcolFee + leftOrder.originFees + rightOrder.originFees) + */ + function getSumFees( + LibPart.Part[] memory originLeft, + LibPart.Part[] memory originRight + ) internal pure returns (uint256) { + uint256 result = 0; + + //adding left origin fees + for (uint256 i; i < originLeft.length; i++) { + result = result + originLeft[i].value; + } + + //adding right origin fees + for (uint256 i; i < originRight.length; i++) { + result = result + originRight[i].value; + } + + return result; + } + + function doTransfersExternal( + LibOrder.Order memory left, + LibOrder.Order memory right + ) external payable returns (uint256 totalLeftValue, uint256 totalRightValue) { + LibOrderDataGeneric.GenericOrderData memory leftData = LibOrderDataGeneric.parse(left); + LibOrderDataGeneric.GenericOrderData memory rightData = LibOrderDataGeneric.parse(right); + + return + doTransfers( + getDealSide(left, leftData), + getDealSide(right, rightData), + getDealData( + left.makeAsset.assetType.assetClass, + right.makeAsset.assetType.assetClass, + left.dataType, + right.dataType, + leftData, + rightData + ) + ); + } +} diff --git a/packages/marketplace/src/exchange/mocks/tokens/ERC2981.sol b/packages/marketplace/src/exchange/mocks/tokens/ERC2981.sol new file mode 100644 index 0000000000..be12f1534e --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/tokens/ERC2981.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (token/common/ERC2981.sol) +// TODO: When we move to oz > 4.5 we can use the file directly from there + +pragma solidity ^0.8.0; + +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC2981} from "../../../royalties/IERC2981.sol"; + +/** + * @dev Implementation of the NFT Royalty Standard, a standardized way to retrieve royalty payment information. + * + * Royalty information can be specified globally for all token ids via {_setDefaultRoyalty}, and/or individually for + * specific token ids via {_setTokenRoyalty}. The latter takes precedence over the first. + * + * Royalty is specified as a fraction of sale price. {_feeDenominator} is overridable but defaults to 10000, meaning the + * fee is specified in basis points by default. + * + * IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See + * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to + * voluntarily pay royalties together with sales, but note that this standard is not yet widely supported. + * + * _Available since v4.5._ + */ +abstract contract ERC2981 is IERC2981, ERC165 { + struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; + } + + RoyaltyInfo private _defaultRoyaltyInfo; + mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo; + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @inheritdoc IERC2981 + */ + function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override returns (address, uint256) { + RoyaltyInfo memory royalty = _tokenRoyaltyInfo[tokenId]; + + if (royalty.receiver == address(0)) { + royalty = _defaultRoyaltyInfo; + } + + uint256 royaltyAmount = (salePrice * royalty.royaltyFraction) / _feeDenominator(); + + return (royalty.receiver, royaltyAmount); + } + + /** + * @dev The denominator with which to interpret the fee set in {_setTokenRoyalty} and {_setDefaultRoyalty} as a + * fraction of the sale price. Defaults to 10000 so fees are expressed in basis points, but may be customized by an + * override. + */ + function _feeDenominator() internal pure virtual returns (uint96) { + return 10000; + } + + /** + * @dev Sets the royalty information that all ids in this contract will default to. + * + * Requirements: + * + * - `receiver` cannot be the zero address. + * - `feeNumerator` cannot be greater than the fee denominator. + */ + function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual { + require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); + require(receiver != address(0), "ERC2981: invalid receiver"); + + _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator); + } + + /** + * @dev Removes default royalty information. + */ + function _deleteDefaultRoyalty() internal virtual { + delete _defaultRoyaltyInfo; + } + + /** + * @dev Sets the royalty information for a specific token id, overriding the global default. + * + * Requirements: + * + * - `receiver` cannot be the zero address. + * - `feeNumerator` cannot be greater than the fee denominator. + */ + function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual { + require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); + require(receiver != address(0), "ERC2981: Invalid parameters"); + + _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator); + } + + /** + * @dev Resets royalty information for the token id back to the global default. + */ + function _resetTokenRoyalty(uint256 tokenId) internal virtual { + delete _tokenRoyaltyInfo[tokenId]; + } +} diff --git a/packages/marketplace/src/exchange/mocks/tokens/MintableERC1155.sol b/packages/marketplace/src/exchange/mocks/tokens/MintableERC1155.sol new file mode 100644 index 0000000000..9ce0ab6074 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/tokens/MintableERC1155.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract MintableERC1155 is ERC1155 { + constructor() ERC1155("http://test.test") {} + + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public { + _mintBatch(to, ids, amounts, data); + } + + function mint(address account, uint256 id, uint256 amount, bytes memory data) public { + _mint(account, id, amount, data); + } +} diff --git a/packages/marketplace/src/exchange/mocks/tokens/MintableERC1155WithRoyalties.sol b/packages/marketplace/src/exchange/mocks/tokens/MintableERC1155WithRoyalties.sol new file mode 100644 index 0000000000..6a3a614f1a --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/tokens/MintableERC1155WithRoyalties.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {MintableERC1155, ERC1155} from "./MintableERC1155.sol"; +import {ERC2981} from "./ERC2981.sol"; + +contract MintableERC1155WithRoyalties is MintableERC1155, ERC2981 { + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, ERC2981) returns (bool) { + return ERC1155.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId); + } + + function setDefaultRoyalty(address receiver, uint96 feeNumerator) external { + _setDefaultRoyalty(receiver, feeNumerator); + } + + function deleteDefaultRoyalty() external { + _deleteDefaultRoyalty(); + } + + function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) external { + _setTokenRoyalty(tokenId, receiver, feeNumerator); + } + + function resetTokenRoyalty(uint256 tokenId) external { + _resetTokenRoyalty(tokenId); + } +} diff --git a/packages/marketplace/src/exchange/mocks/tokens/MintableERC20.sol b/packages/marketplace/src/exchange/mocks/tokens/MintableERC20.sol new file mode 100644 index 0000000000..53a7fe54dd --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/tokens/MintableERC20.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MintableERC20 is ERC20 { + constructor() ERC20("MINE", "MINE") {} + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/packages/marketplace/src/exchange/mocks/tokens/MintableERC721.sol b/packages/marketplace/src/exchange/mocks/tokens/MintableERC721.sol new file mode 100644 index 0000000000..aaddaa0cbf --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/tokens/MintableERC721.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract MintableERC721 is ERC721 { + constructor() ERC721("MINFT", "MINFT") {} + + function safeMint(address to, uint256 tokenId) public { + _safeMint(to, tokenId); + } +} diff --git a/packages/marketplace/src/exchange/mocks/tokens/MintableERC721WithRoyalties.sol b/packages/marketplace/src/exchange/mocks/tokens/MintableERC721WithRoyalties.sol new file mode 100644 index 0000000000..77084201b3 --- /dev/null +++ b/packages/marketplace/src/exchange/mocks/tokens/MintableERC721WithRoyalties.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {MintableERC721, ERC721} from "./MintableERC721.sol"; +import {ERC2981} from "./ERC2981.sol"; + +contract MintableERC721WithRoyalties is MintableERC721, ERC2981 { + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC2981) returns (bool) { + return ERC721.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId); + } + + function setDefaultRoyalty(address receiver, uint96 feeNumerator) external { + _setDefaultRoyalty(receiver, feeNumerator); + } + + function deleteDefaultRoyalty() external { + _deleteDefaultRoyalty(); + } + + function setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) external { + _setTokenRoyalty(tokenId, receiver, feeNumerator); + } + + function resetTokenRoyalty(uint256 tokenId) external { + _resetTokenRoyalty(tokenId); + } +} diff --git a/packages/marketplace/src/interfaces/IAssetMatcher.sol b/packages/marketplace/src/interfaces/IAssetMatcher.sol new file mode 100644 index 0000000000..4dcb0098de --- /dev/null +++ b/packages/marketplace/src/interfaces/IAssetMatcher.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibAsset} from "../lib-asset/LibAsset.sol"; + +/// @title interface for AssetMatcher +/// @notice contains the signature for matchAssets that verifies if order assets match +interface IAssetMatcher { + /// @notice matchAssets function + /// @param leftAssetType left order + /// @param rightAssetType right order + /// @return AssetType of matched asset + function matchAssets( + LibAsset.AssetType memory leftAssetType, + LibAsset.AssetType memory rightAssetType + ) external view returns (LibAsset.AssetType memory); +} diff --git a/packages/marketplace/src/interfaces/IOrderValidator.sol b/packages/marketplace/src/interfaces/IOrderValidator.sol new file mode 100644 index 0000000000..60c165ccea --- /dev/null +++ b/packages/marketplace/src/interfaces/IOrderValidator.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibOrder} from "../lib-order/LibOrder.sol"; + +/// @title interface for the OrderValidator contract +/// @notice contains the signature for validate, isPurchaseValid and verifyERC20Whitelist functions +interface IOrderValidator { + /// @notice verifies order + /// @param order order to be validated + /// @param signature signature of order + /// @param sender order sender + function validate(LibOrder.Order memory order, bytes memory signature, address sender) external view; + + /// @notice verifies if backend signature is valid + /// @param orderBack order to be validated + /// @param signature signature of order + /// @return boolean comparison between the recover signature and signing wallet + function isPurchaseValid(LibOrder.OrderBack memory orderBack, bytes memory signature) external view returns (bool); + + /// @notice if ERC20 token is accepted + /// @param tokenAddress ERC20 token address + function verifyERC20Whitelist(address tokenAddress) external view; +} diff --git a/packages/marketplace/src/interfaces/IRoyaltiesProvider.sol b/packages/marketplace/src/interfaces/IRoyaltiesProvider.sol new file mode 100644 index 0000000000..e0bf01d252 --- /dev/null +++ b/packages/marketplace/src/interfaces/IRoyaltiesProvider.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibPart} from "../lib-part/LibPart.sol"; + +/// @title interface for the RoyaltiesProvider contract +/// @notice contains the signature for the getRoyalties function +interface IRoyaltiesProvider { + /// @notice calculates all roaylties in token for tokenId + /// @param token address of token + /// @param tokenId of the token we want to calculate royalites + /// @return a LibPart.Part with allroyalties for token + function getRoyalties(address token, uint256 tokenId) external returns (LibPart.Part[] memory); +} diff --git a/packages/marketplace/src/interfaces/IWhiteList.sol b/packages/marketplace/src/interfaces/IWhiteList.sol new file mode 100644 index 0000000000..eed66cbaaa --- /dev/null +++ b/packages/marketplace/src/interfaces/IWhiteList.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title interface for the WhiteList contract +/// @notice contains the signature for the contract function +interface IWhiteList { + /// @notice if status == tsbOnly, then only tsbListedContracts [small mapping] + /// @return tsbOnly + function tsbOnly() external view returns (bool); + + /// @notice if status == partners, then tsbListedContracts and partnerContracts [manageable mapping] + /// @return partners + function partners() external view returns (bool); + + // @notice if status == open, then no whitelist [no mapping needed]. But then we need a removeListing function for contracts we subsequently + /// @return open + function open() external view returns (bool); + + /// @notice if status == erc20List, users can only pay white whitelisted ERC20 tokens + /// @return erc20List + function erc20List() external view returns (bool); + + /// @notice mapping containing the list of contracts in the tsb white list + /// @return true if list contains address + function tsbWhiteList(address) external view returns (bool); + + /// @notice mapping containing the list of contracts in the partners white list + /// @return true if list contains address + function partnerWhiteList(address) external view returns (bool); + + /// @notice mapping containing the list of contracts in the erc20 white list + /// @return true if list contains address + function erc20WhiteList(address) external view returns (bool); +} diff --git a/packages/marketplace/src/lazy-mint/erc-1155/IERC1155LazyMint.sol b/packages/marketplace/src/lazy-mint/erc-1155/IERC1155LazyMint.sol new file mode 100644 index 0000000000..03b52a622d --- /dev/null +++ b/packages/marketplace/src/lazy-mint/erc-1155/IERC1155LazyMint.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; +import {LibERC1155LazyMint} from "./LibERC1155LazyMint.sol"; +import {LibPart} from "../../lib-part/LibPart.sol"; + +/// @title interface for 1155LazyMint +/// @notice contains function signatures for mintAndTransfer and transferFromOrMint +interface IERC1155LazyMint is IERC1155Upgradeable { + event Supply(uint256 tokenId, uint256 value); + event Creators(uint256 tokenId, LibPart.Part[] creators); + + /// @notice function to mintAndTransfer + /// @param data mint data for ERC1155 + /// @param to address that will receive the minted token + /// @param amount amount of tokens + function mintAndTransfer(LibERC1155LazyMint.Mint1155Data memory data, address to, uint256 amount) external; + + /// @notice function that transfer a token if already exists, otherwise mint and transfer it + /// @param data token data + /// @param from address from which the token is taken or transferred + /// @param to address that receives the token + /// @param amount amount of tokens + function transferFromOrMint( + LibERC1155LazyMint.Mint1155Data memory data, + address from, + address to, + uint256 amount + ) external; +} diff --git a/packages/marketplace/src/lazy-mint/erc-1155/LibERC1155LazyMint.sol b/packages/marketplace/src/lazy-mint/erc-1155/LibERC1155LazyMint.sol new file mode 100644 index 0000000000..16592babc9 --- /dev/null +++ b/packages/marketplace/src/lazy-mint/erc-1155/LibERC1155LazyMint.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibPart} from "../../lib-part/LibPart.sol"; + +/// @title library for ERC1155 lazy minting +/// @notice contains struct for ERC1155 mint data and hash function for said data +library LibERC1155LazyMint { + /// @notice hash identifier of ERC1155 lazy asset class + /// @return ERC1155_LAZY_ASSET_CLASS identifier + bytes4 public constant ERC1155_LAZY_ASSET_CLASS = bytes4(keccak256("ERC1155_LAZY")); + + struct Mint1155Data { + uint256 tokenId; + string tokenURI; + uint256 supply; + LibPart.Part[] creators; + LibPart.Part[] royalties; + bytes[] signatures; + } + + /// @notice type hash of mint and transfer + /// @return typehash of functions + bytes32 public constant MINT_AND_TRANSFER_TYPEHASH = + keccak256( + "Mint1155(uint256 tokenId,uint256 supply,string tokenURI,Part[] creators,Part[] royalties)Part(address account,uint96 value)" + ); + + /// @notice hash function for Mint1155Data + /// @param data Mint1155Data to be hashed + /// @return bytes32 hash of data + function hash(Mint1155Data memory data) internal pure returns (bytes32) { + bytes32[] memory royaltiesBytes = new bytes32[](data.royalties.length); + for (uint256 i = 0; i < data.royalties.length; ++i) { + royaltiesBytes[i] = LibPart.hash(data.royalties[i]); + } + bytes32[] memory creatorsBytes = new bytes32[](data.creators.length); + for (uint256 i = 0; i < data.creators.length; ++i) { + creatorsBytes[i] = LibPart.hash(data.creators[i]); + } + return + keccak256( + abi.encode( + MINT_AND_TRANSFER_TYPEHASH, + data.tokenId, + data.supply, + keccak256(bytes(data.tokenURI)), + keccak256(abi.encodePacked(creatorsBytes)), + keccak256(abi.encodePacked(royaltiesBytes)) + ) + ); + } +} diff --git a/packages/marketplace/src/lazy-mint/erc-721/IERC721LazyMint.sol b/packages/marketplace/src/lazy-mint/erc-721/IERC721LazyMint.sol new file mode 100644 index 0000000000..6b5ef89683 --- /dev/null +++ b/packages/marketplace/src/lazy-mint/erc-721/IERC721LazyMint.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {LibERC721LazyMint} from "./LibERC721LazyMint.sol"; +import {LibPart} from "../../lib-part/LibPart.sol"; + +/// @title interface for ERC721LazyMint +/// @notice contains function signatures for mintAndTransfer and transferFromOrMint +interface IERC721LazyMint is IERC721Upgradeable { + /// @notice event for listing the creators or fee partakers of a token + /// @param tokenId uint256 token identifier + /// @param creators array of participants + event Creators(uint256 tokenId, LibPart.Part[] creators); + + /// @notice function to mintAndTransfer + /// @param data mint data for ERC721 + /// @param to address that will receive the minted token + function mintAndTransfer(LibERC721LazyMint.Mint721Data memory data, address to) external; + + /// @notice function that transfer a token if already exists, otherwise mint and transfer it + /// @param data token data + /// @param from address from which the token is taken or transferred + /// @param to address that receives the token + function transferFromOrMint(LibERC721LazyMint.Mint721Data memory data, address from, address to) external; +} diff --git a/packages/marketplace/src/lazy-mint/erc-721/LibERC721LazyMint.sol b/packages/marketplace/src/lazy-mint/erc-721/LibERC721LazyMint.sol new file mode 100644 index 0000000000..4b2b978f20 --- /dev/null +++ b/packages/marketplace/src/lazy-mint/erc-721/LibERC721LazyMint.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibPart} from "../../lib-part/LibPart.sol"; + +/// @title library for ERC721 lazy minting +/// @notice contains struct for ERC721 mint data and hash function for said data +library LibERC721LazyMint { + /// @notice hash identifier of ERC721 lazy asset class + /// @return ERC721_LAZY_ASSET_CLASS identifier + bytes4 public constant ERC721_LAZY_ASSET_CLASS = bytes4(keccak256("ERC721_LAZY")); + + struct Mint721Data { + uint256 tokenId; + string tokenURI; + LibPart.Part[] creators; + LibPart.Part[] royalties; + bytes[] signatures; + } + + /// @notice type hash of mint and transfer + /// @return typehash of functions + bytes32 public constant MINT_AND_TRANSFER_TYPEHASH = + keccak256( + "Mint721(uint256 tokenId,string tokenURI,Part[] creators,Part[] royalties)Part(address account,uint96 value)" + ); + + /// @notice hash function for Mint721Data + /// @param data Mint721Data to be hashed + /// @return bytes32 hash of data + function hash(Mint721Data memory data) internal pure returns (bytes32) { + bytes32[] memory royaltiesBytes = new bytes32[](data.royalties.length); + for (uint256 i = 0; i < data.royalties.length; ++i) { + royaltiesBytes[i] = LibPart.hash(data.royalties[i]); + } + bytes32[] memory creatorsBytes = new bytes32[](data.creators.length); + for (uint256 i = 0; i < data.creators.length; ++i) { + creatorsBytes[i] = LibPart.hash(data.creators[i]); + } + return + keccak256( + abi.encode( + MINT_AND_TRANSFER_TYPEHASH, + data.tokenId, + keccak256(bytes(data.tokenURI)), + keccak256(abi.encodePacked(creatorsBytes)), + keccak256(abi.encodePacked(royaltiesBytes)) + ) + ); + } +} diff --git a/packages/marketplace/src/lazy-mint/mocks/ERC1155LazyMintTest.sol b/packages/marketplace/src/lazy-mint/mocks/ERC1155LazyMintTest.sol new file mode 100644 index 0000000000..f1c19818ea --- /dev/null +++ b/packages/marketplace/src/lazy-mint/mocks/ERC1155LazyMintTest.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import {IERC1155LazyMint} from "../erc-1155/IERC1155LazyMint.sol"; +import {LibERC1155LazyMint} from "../erc-1155/LibERC1155LazyMint.sol"; + +contract ERC1155LazyMintTest is IERC1155LazyMint, ERC1155Upgradeable { + function mintAndTransfer( + LibERC1155LazyMint.Mint1155Data memory data, + address to, + uint256 _amount + ) external override { + _mint(to, data.tokenId, _amount, ""); + } + + function transferFromOrMint( + LibERC1155LazyMint.Mint1155Data memory data, + address from, + address to, + uint256 amount + ) external override { + uint256 balance = balanceOf(from, data.tokenId); + if (balance != 0) { + safeTransferFrom(from, to, data.tokenId, amount, ""); + } else { + this.mintAndTransfer(data, to, amount); + } + } + + function encode(LibERC1155LazyMint.Mint1155Data memory data) external view returns (bytes memory) { + return abi.encode(address(this), data); + } +} diff --git a/packages/marketplace/src/lazy-mint/mocks/ERC1155Test.sol b/packages/marketplace/src/lazy-mint/mocks/ERC1155Test.sol new file mode 100644 index 0000000000..b356cb2890 --- /dev/null +++ b/packages/marketplace/src/lazy-mint/mocks/ERC1155Test.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibERC1155LazyMint} from "../erc-1155/LibERC1155LazyMint.sol"; +import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; + +contract ERC1155Test is EIP712Upgradeable { + using ECDSAUpgradeable for bytes32; + + function __ERC1155Test_init() external initializer { + __EIP712_init("Mint1155", "1"); + } + + function recover( + LibERC1155LazyMint.Mint1155Data memory data, + bytes memory signature + ) external view returns (address) { + bytes32 structHash = LibERC1155LazyMint.hash(data); + bytes32 hash = _hashTypedDataV4(structHash); + return hash.recover(signature); + } +} diff --git a/packages/marketplace/src/lazy-mint/mocks/ERC721LazyMintTest.sol b/packages/marketplace/src/lazy-mint/mocks/ERC721LazyMintTest.sol new file mode 100644 index 0000000000..c8ee532482 --- /dev/null +++ b/packages/marketplace/src/lazy-mint/mocks/ERC721LazyMintTest.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {IERC721LazyMint, LibERC721LazyMint} from "../erc-721/IERC721LazyMint.sol"; + +contract ERC721LazyMintTest is IERC721LazyMint, ERC721Upgradeable { + function mintAndTransfer(LibERC721LazyMint.Mint721Data memory data, address to) external override { + _mint(to, data.tokenId); + } + + function transferFromOrMint(LibERC721LazyMint.Mint721Data memory data, address from, address to) external override { + if (_exists(data.tokenId)) { + safeTransferFrom(from, to, data.tokenId); + } else { + this.mintAndTransfer(data, to); + } + } + + function encode(LibERC721LazyMint.Mint721Data memory data) external view returns (bytes memory) { + return abi.encode(address(this), data); + } +} diff --git a/packages/marketplace/src/lazy-mint/mocks/ERC721Test.sol b/packages/marketplace/src/lazy-mint/mocks/ERC721Test.sol new file mode 100644 index 0000000000..4810555fd4 --- /dev/null +++ b/packages/marketplace/src/lazy-mint/mocks/ERC721Test.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibERC721LazyMint} from "../erc-721/LibERC721LazyMint.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; + +contract ERC721Test is EIP712Upgradeable { + using ECDSAUpgradeable for bytes32; + + function __ERC721Test_init() external initializer { + __EIP712_init("Mint721", "1"); + } + + function recover( + LibERC721LazyMint.Mint721Data memory data, + bytes memory signature + ) external view returns (address) { + bytes32 structHash = LibERC721LazyMint.hash(data); + bytes32 hash = _hashTypedDataV4(structHash); + return hash.recover(signature); + } +} diff --git a/packages/marketplace/src/lib-asset/LibAsset.md b/packages/marketplace/src/lib-asset/LibAsset.md new file mode 100644 index 0000000000..d3695264a1 --- /dev/null +++ b/packages/marketplace/src/lib-asset/LibAsset.md @@ -0,0 +1,18 @@ +#### Features + +`LibAsset` contains struct `Asset` and `AssetType`. + +`Asset` represents any asset on ethereum blockchain. `Asset` has type and value (amount of an asset). + +`AssetType` is a type of a specific asset. For example `AssetType` is specific ERC-721 token (key is token + tokenId) or specific ERC-20 token (DAI for example). +It consists of `asset class` and generic data (format of data is different for different asset classes). For example, for asset class `ERC20` data holds address of the token, for ERC-721 data holds smart contract address and tokenId. + +`Asset` fields: +- `AssetType` assetType +- `uint` value + +`AssetType` fields: +- `bytes4` assetClass +- `bytes` data + +`Asset` is used in the [LibOrder](../exchange/libraries/LibOrder.md) \ No newline at end of file diff --git a/packages/marketplace/src/lib-asset/LibAsset.sol b/packages/marketplace/src/lib-asset/LibAsset.sol new file mode 100644 index 0000000000..f9adef311d --- /dev/null +++ b/packages/marketplace/src/lib-asset/LibAsset.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title library for Assets +/// @notice contains structs for Asset and AssetType +/// @dev Asset represents any asset on ethereum blockchain. +/// @dev AssetType is a type of a specific asset +library LibAsset { + bytes4 public constant ETH_ASSET_CLASS = bytes4(keccak256("ETH")); + bytes4 public constant ERC20_ASSET_CLASS = bytes4(keccak256("ERC20")); + bytes4 public constant ERC721_ASSET_CLASS = bytes4(keccak256("ERC721")); + bytes4 public constant ERC1155_ASSET_CLASS = bytes4(keccak256("ERC1155")); + bytes4 public constant ERC721_TSB_CLASS = bytes4(keccak256("ERC721_TSB")); + bytes4 public constant ERC1155_TSB_CLASS = bytes4(keccak256("ERC1155_TSB")); + bytes4 public constant BUNDLE = bytes4(keccak256("BUNDLE")); + bytes4 public constant COLLECTION = bytes4(keccak256("COLLECTION")); + + bytes32 internal constant ASSET_TYPE_TYPEHASH = keccak256("AssetType(bytes4 assetClass,bytes data)"); + + bytes32 internal constant ASSET_TYPEHASH = + keccak256("Asset(AssetType assetType,uint256 value)AssetType(bytes4 assetClass,bytes data)"); + + struct AssetType { + bytes4 assetClass; + bytes data; + } + + struct Asset { + AssetType assetType; + uint256 value; + } + + /// @notice calculate hash of asset type + /// @param assetType to be hashed + /// @return hash of assetType + function hash(AssetType memory assetType) internal pure returns (bytes32) { + return keccak256(abi.encode(ASSET_TYPE_TYPEHASH, assetType.assetClass, keccak256(assetType.data))); + } + + /// @notice calculate hash of asset + /// @param asset to be hashed + /// @return hash of asset + function hash(Asset memory asset) internal pure returns (bytes32) { + return keccak256(abi.encode(ASSET_TYPEHASH, hash(asset.assetType), asset.value)); + } +} diff --git a/packages/marketplace/src/lib-bp/BpLibrary.sol b/packages/marketplace/src/lib-bp/BpLibrary.sol new file mode 100644 index 0000000000..b2d6802080 --- /dev/null +++ b/packages/marketplace/src/lib-bp/BpLibrary.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title library for Base Point calculation +/// @notice contains a method for basepoint calculation +library BpLibrary { + /// @notice basepoint calculation + /// @param value value to be multiplied by basepoint + /// @param bpValue basepoint value + /// @return value times basepoint divided by 10000 + function bp(uint256 value, uint256 bpValue) internal pure returns (uint256) { + return (value * bpValue) / 10000; + } +} diff --git a/packages/marketplace/src/lib-order/LibMath.sol b/packages/marketplace/src/lib-order/LibMath.sol new file mode 100644 index 0000000000..7ee6553781 --- /dev/null +++ b/packages/marketplace/src/lib-order/LibMath.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +library LibMath { + /// @dev Calculates partial value given a numerator and denominator rounded down. + /// Reverts if rounding error is >= 0.1% + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return partialAmount value of target rounded down. + function safeGetPartialAmountFloor( + uint256 numerator, + uint256 denominator, + uint256 target + ) internal pure returns (uint256 partialAmount) { + if (isRoundingErrorFloor(numerator, denominator, target)) { + revert("rounding error"); + } + partialAmount = (numerator * target) / (denominator); + } + + /// @dev Checks if rounding error >= 0.1% when rounding down. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to multiply with numerator/denominator. + /// @return isError Rounding error is present. + function isRoundingErrorFloor( + uint256 numerator, + uint256 denominator, + uint256 target + ) internal pure returns (bool isError) { + if (denominator == 0) { + revert("division by zero"); + } + + // The absolute rounding error is the difference between the rounded + // value and the ideal value. The relative rounding error is the + // absolute rounding error divided by the absolute value of the + // ideal value. This is undefined when the ideal value is zero. + // + // The ideal value is `numerator * target / denominator`. + // Let's call `numerator * target % denominator` the remainder. + // The absolute error is `remainder / denominator`. + // + // When the ideal value is zero, we require the absolute error to + // be zero. Fortunately, this is always the case. The ideal value is + // zero iff `numerator == 0` and/or `target == 0`. In this case the + // remainder and absolute error are also zero. + if (target == 0 || numerator == 0) { + return false; + } + + // Otherwise, we want the relative rounding error to be strictly + // less than 0.1%. + // The relative error is `remainder / (numerator * target)`. + // We want the relative error less than 1 / 1000: + // remainder / (numerator * target) < 1 / 1000 + // or equivalently: + // 1000 * remainder < numerator * target + // so we have a rounding error iff: + // 1000 * remainder >= numerator * target + uint256 remainder = mulmod(target, numerator, denominator); + isError = remainder * 1000 >= numerator * target; + } + + function safeGetPartialAmountCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) internal pure returns (uint256 partialAmount) { + if (isRoundingErrorCeil(numerator, denominator, target)) { + revert("rounding error"); + } + partialAmount = (numerator * target) + ((denominator - 1) / denominator); + } + + /// @dev Checks if rounding error >= 0.1% when rounding up. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to multiply with numerator/denominator. + /// @return isError Rounding error is present. + function isRoundingErrorCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) internal pure returns (bool isError) { + if (denominator == 0) { + revert("division by zero"); + } + + // See the comments in `isRoundingError`. + if (target == 0 || numerator == 0) { + // When either is zero, the ideal value and rounded value are zero + // and there is no rounding error. (Although the relative error + // is undefined.) + return false; + } + // Compute remainder as before + uint256 remainder = mulmod(target, numerator, denominator); + remainder = (denominator - remainder) % denominator; + isError = remainder * 1000 >= numerator * target; + return isError; + } +} diff --git a/packages/marketplace/src/lib-order/LibOrder.sol b/packages/marketplace/src/lib-order/LibOrder.sol new file mode 100644 index 0000000000..002cebda8e --- /dev/null +++ b/packages/marketplace/src/lib-order/LibOrder.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibAsset} from "../lib-asset/LibAsset.sol"; +import {LibMath} from "./LibMath.sol"; + +/// @title library for Order +/// @notice contains structs and functions related to Order +library LibOrder { + bytes32 internal constant ORDER_TYPEHASH = + keccak256( + "Order(address maker,Asset makeAsset,address taker,Asset takeAsset,uint256 salt,uint256 start,uint256 end,bytes4 dataType,bytes data)Asset(AssetType assetType,uint256 value)AssetType(bytes4 assetClass,bytes data)" + ); + + bytes32 internal constant ORDER_BACK_TYPEHASH = + keccak256( + "OrderBack(address buyer,address maker,Asset makeAsset,address taker,Asset takeAsset,uint256 salt,uint256 start,uint256 end,bytes4 dataType,bytes data)Asset(AssetType assetType,uint256 value)AssetType(bytes4 assetClass,bytes data)" + ); + + bytes4 internal constant DEFAULT_ORDER_TYPE = 0xffffffff; + + struct Order { + address maker; + LibAsset.Asset makeAsset; + address taker; + LibAsset.Asset takeAsset; + uint256 salt; + uint256 start; + uint256 end; + bytes4 dataType; + bytes data; + } + + struct OrderBack { + address buyer; + address maker; + LibAsset.Asset makeAsset; + address taker; + LibAsset.Asset takeAsset; + uint256 salt; + uint256 start; + uint256 end; + bytes4 dataType; + bytes data; + } + + /// @notice calculate the remaining fill from orders + /// @param order order that we will calculate the remaining fill + /// @param fill to be subtracted + /// @param isMakeFill if true take fill from make side, if false from take + /// @return makeValue remaining fill from make side + /// @return takeValue remaining fill from take side + function calculateRemaining( + Order memory order, + uint256 fill, + bool isMakeFill + ) internal pure returns (uint256 makeValue, uint256 takeValue) { + if (isMakeFill) { + makeValue = order.makeAsset.value - fill; + takeValue = LibMath.safeGetPartialAmountFloor(order.takeAsset.value, order.makeAsset.value, makeValue); + } else { + takeValue = order.takeAsset.value - fill; + makeValue = LibMath.safeGetPartialAmountFloor(order.makeAsset.value, order.takeAsset.value, takeValue); + } + } + + /// @notice calculate hash key from order + /// @param order object to be hashed + /// @return hash key of order + function hashKey(Order memory order) internal pure returns (bytes32) { + if (order.dataType == DEFAULT_ORDER_TYPE) { + return + keccak256( + abi.encode( + order.maker, + LibAsset.hash(order.makeAsset.assetType), + LibAsset.hash(order.takeAsset.assetType), + order.salt + ) + ); + } else { + //order.data is in hash for V3 and all new order + return + keccak256( + abi.encode( + order.maker, + LibAsset.hash(order.makeAsset.assetType), + LibAsset.hash(order.takeAsset.assetType), + order.salt, + order.data + ) + ); + } + } + + /// @notice calculate hash from order + /// @param order object to be hashed + /// @return hash of order + function hash(Order memory order) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + ORDER_TYPEHASH, + order.maker, + LibAsset.hash(order.makeAsset), + order.taker, + LibAsset.hash(order.takeAsset), + order.salt, + order.start, + order.end, + order.dataType, + keccak256(order.data) + ) + ); + } + + /// @notice calculate hash from backend order + /// @param order object to be hashed + /// @return hash key of order + function backendHash(OrderBack memory order) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + ORDER_BACK_TYPEHASH, + order.buyer, + order.maker, + LibAsset.hash(order.makeAsset), + order.taker, + LibAsset.hash(order.takeAsset), + order.salt, + order.start, + order.end, + order.dataType, + keccak256(order.data) + ) + ); + } + + /// @notice validates order time + /// @param order whose time we want to validate + function validateOrderTime(LibOrder.Order memory order) internal view { + require(order.start == 0 || order.start < block.timestamp, "Order start validation failed"); + require(order.end == 0 || order.end > block.timestamp, "Order end validation failed"); + } +} diff --git a/packages/marketplace/src/lib-order/LibOrderData.md b/packages/marketplace/src/lib-order/LibOrderData.md new file mode 100644 index 0000000000..1e29d2d9e7 --- /dev/null +++ b/packages/marketplace/src/lib-order/LibOrderData.md @@ -0,0 +1,75 @@ +# Features + +## Data types, corresponding transfers/fees logic +`Order` data can be generic. `dataType` field defines format of that data. +- `"0xffffffff"` or `"no type"` + - no data + - fees logic + - no fees +- `"V1"` + - fields + - `LibPart.Part[] payouts` + - array of payouts, i.e. how takeAsset of the order is going to be distributed. (usually 100% goes to order.maker, can be something like 50% goes to maker, 50% to someone else. it can be divided in any other way) + - `LibPart.Part[] originFees` + - additional fees (e.g. 5% of the payment goes to additional address) + - fees logic + - `originFees` from buy-order is taken from the buyer, `originFees` from sell-order is taken from the seller. e.g. sell order is `1 ERC721` => `100 ETH`, buy order is `100 ETH` => `1 ERC721`. Buy order has `originFees` = [`{5% to addr1}`,`{10% to addr2}`]. Sell order has `originFees` = [`{5% to addr3}`]. Then, total amount that buyer needs to send is `100 ETH` + `5%*100ETH` + `10%*100ETH` = `115 ETH` (15% more than order value, buy-order `origin fees` are added. So buyer pays for their `origin fees`). From this amount `5 ETH` will be transferred to addr1, `10 ETH` to addr2 (now we have `100ETH` left) and `5ETH` to addr3 (it is seller `origin fee`, so it is taken from their part). + - after that NFT `royalties` are taken + - what's left after that is distributed according to sell-order `payouts` +- `"V2"` + - fields + - `LibPart.Part[] payouts`, works the same as in `V1` orders + - `LibPart.Part[] originFees`, works the same as in `V1` orders + - `bool isMakeFill` + - if false, order's `fill` (what part of order is completed, stored on-chain) is calculated from take side (in `V1` orders it always works like that) + - if true, `fill` is calculated from the make side of the order + - fees logic, works the same as in `V1` orders +- `"V3"` two types of `V3` orders. + - `"V3_BUY"` + - fields + - `uint payouts`, works the same as in `V1` orders, but there is only 1 value and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeFirst`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeSecond`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `bytes32 marketplaceMarker`, bytes32 id marketplace, which generate this order + - `"V3_SELL"` + - fields + - `uint payouts`, works the same as in `V1` orders, but there is only 1 value and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeFirst`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint originFeeSecond`, instead of array there can only be 2 originFee in different variables (originFeeFirst and originFeeSecond), and address + amount are encoded into uint (first 12 bytes for amount, last 20 bytes for address), not using `LibPart.Part` struct + - `uint maxFeesBasePoint` + - maximum amount of fees that can be taken from payment (e.g. 10%) + - chosen by seller, that's why it's only present in `V3_SELL` orders + - `maxFeesBasePoint` should be more than `0` + - `maxFeesBasePoint` should not be bigger than `10%` + - `bytes32 marketplaceMarker`, bytes32 id marketplace, which generate this order + - `V3` orders can only be matched if buy-order is `V3_BUY` and the sell-order is `V3_SELL` + - `V3` orders don't have `isMakeFill` field + - `V3_SELL` orders' fills are always calculated from make side (as if `isMakeFill` = true) + - `V3_BUY` orders' fills are always calculated from take side (as if `isMakeFill` = false) + - fees logic + - `V3` orders' fees work differently from all previous orders types + - `originFees` are taken from seller side only. + - sum of buy-order `originFees` + sell-order `originFees` should not be bigger than `maxFeesBasePoint` + - example: + - sell order is `1 ERC721` => `100 ETH` + - `maxFeesBasePoint` is 10 % + - Sell order has `originFeeFirst` = `{2% to addr3}` + - buy order is `100 ETH` => `1 ERC721` + - Buy order has + - `originFeeFirst` = `{3% to addr1}`, + - `originFeeSecond` = `{2% to addr2}` + - total amount for buyer is not affected by fees. it remains `100 ETH` + - `3% * 100 ETH` + `2% * 100 ETH` is taken as buy order's origin fee, `95 ETH` remaining + - `2% * 100 ETH` is taken as sell order's origin, `93 ETH` remaining + - after that NFT `royalties` are taken (same as with previous orders' types) + - what's left after that is distributed according to sell-order `payouts` (same as with previous orders' types) + + + +## Data parsing + +LibOrderData defines function parse which parses data field (according to dataType) and converts any version of the data to the GenericOrderData struct. +(see [LibOrder](LibOrder.md) `Order.data` field) + + + diff --git a/packages/marketplace/src/lib-order/LibOrderData.sol b/packages/marketplace/src/lib-order/LibOrderData.sol new file mode 100644 index 0000000000..60a3e5e9bf --- /dev/null +++ b/packages/marketplace/src/lib-order/LibOrderData.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title library for order data types +/// @notice Data types, corresponding transfers/fees logic +library LibOrderData { + /// @notice hash for order data type sell + /// @return SELL hash + bytes4 public constant SELL = bytes4(keccak256("SELL")); + + /// @notice hash for order data type buy + /// @return BUY hash + bytes4 public constant BUY = bytes4(keccak256("BUY")); + + struct DataSell { + uint256 payouts; + uint256 originFeeFirst; + uint256 originFeeSecond; + uint256 maxFeesBasePoint; + bytes32 marketplaceMarker; + } + + struct DataBuy { + uint256 payouts; + uint256 originFeeFirst; + uint256 originFeeSecond; + bytes32 marketplaceMarker; + } +} diff --git a/packages/marketplace/src/lib-part/LibPart.sol b/packages/marketplace/src/lib-part/LibPart.sol new file mode 100644 index 0000000000..f779793815 --- /dev/null +++ b/packages/marketplace/src/lib-part/LibPart.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title library for parts of transaction fees +/// @notice contains the struct for Part, containing the fee recipient and value +library LibPart { + /// @notice type hash of Part struct + /// @return hash of Part struct + bytes32 public constant TYPE_HASH = keccak256("Part(address account,uint96 value)"); + + struct Part { + address payable account; + uint96 value; + } + + /// @notice hash part object + /// @param part to be hashed + /// @return resulting hash + function hash(Part memory part) internal pure returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, part.account, part.value)); + } +} diff --git a/packages/marketplace/src/royalties-registry/IMultiRoyaltyRecipients.sol b/packages/marketplace/src/royalties-registry/IMultiRoyaltyRecipients.sol new file mode 100644 index 0000000000..6ab548d5cf --- /dev/null +++ b/packages/marketplace/src/royalties-registry/IMultiRoyaltyRecipients.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/// @title interface for MultiRoyaltyRecipients +/// @notice Multi-receiver EIP2981 reference override implementation +interface IMultiRoyaltyRecipients is IERC165 { + struct Recipient { + address payable recipient; + uint16 bps; + } + + /// @notice get recipients of token royalties + /// @param tokenId token identifier + /// @return array of royalties recipients + function getRecipients(uint256 tokenId) external view returns (Recipient[] memory); +} diff --git a/packages/marketplace/src/royalties-registry/Migrations.sol b/packages/marketplace/src/royalties-registry/Migrations.sol new file mode 100644 index 0000000000..65ec014306 --- /dev/null +++ b/packages/marketplace/src/royalties-registry/Migrations.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +contract Migrations { + address public owner = msg.sender; + uint256 public lastCompletedMigration; + + modifier restricted() { + require(msg.sender == owner, "This function is restricted to the contract's owner"); + _; + } + + function setCompleted(uint256 completed) public restricted { + lastCompletedMigration = completed; + } +} diff --git a/packages/marketplace/src/royalties-registry/RoyaltiesRegistry.sol b/packages/marketplace/src/royalties-registry/RoyaltiesRegistry.sol new file mode 100644 index 0000000000..e47bc988ef --- /dev/null +++ b/packages/marketplace/src/royalties-registry/RoyaltiesRegistry.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IMultiRoyaltyRecipients} from "./IMultiRoyaltyRecipients.sol"; +import {IRoyaltiesProvider} from "../interfaces/IRoyaltiesProvider.sol"; +import {LibRoyalties2981} from "../royalties/LibRoyalties2981.sol"; +import {LibPart} from "../lib-part/LibPart.sol"; +import {IERC2981} from "../royalties/IERC2981.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/// @title royalties registry contract +/// @notice contract allows to processing different types of royalties +contract RoyaltiesRegistry is IRoyaltiesProvider, OwnableUpgradeable { + /// @notice deprecated + /// @param token deprecated + /// @param tokenId deprecated + /// @param royalties deprecated + event RoyaltiesSetForToken(address indexed token, uint256 indexed tokenId, LibPart.Part[] royalties); + + /// @notice emitted when royalties is set for token + /// @param token token address + /// @param royalties array of royalties + event RoyaltiesSetForContract(address indexed token, LibPart.Part[] royalties); + + /// @dev struct to store royalties in royaltiesByToken + struct RoyaltiesSet { + bool initialized; + LibPart.Part[] royalties; + } + + bytes4 internal constant INTERFACE_ID_GET_RECIPIENTS = 0xfd90e897; + + /// @notice deprecated + mapping(bytes32 => RoyaltiesSet) public royaltiesByTokenAndTokenId; + + /// @notice stores royalties for token contract, set in setRoyaltiesByToken() method + mapping(address => RoyaltiesSet) public royaltiesByToken; + + /// @notice stores external provider and royalties type for token contract + /// @return royaltiesProviders external providers + mapping(address => uint256) public royaltiesProviders; + + /// @dev total amount or supported royalties types + /// @dev 0 - royalties type is unset + /// @dev 1 - royaltiesByToken + /// @dev 2 - external provider + /// @dev 3 - EIP-2981 + /// @dev 4 - unsupported/nonexistent royalties type + uint256 internal constant ROYALTIES_TYPES_AMOUNT = 4; + + /// @notice Royalties registry initializer + function __RoyaltiesRegistry_init() external initializer { + __Ownable_init(); + } + + /// @notice sets external provider for token contract, and royalties type = 4 + /// @param token token address + /// @param provider address of provider + function setProviderByToken(address token, address provider) external { + checkOwner(token); + setRoyaltiesType(token, 2, provider); + } + + /// @notice returns provider address for token contract from royaltiesProviders mapping + /// @param token token address + /// @return address of provider + function getProvider(address token) public view returns (address) { + return address(uint160(royaltiesProviders[token])); + } + + /// @notice returns royalties type for token contract + /// @param token token address + /// @return royalty type + function getRoyaltiesType(address token) external view returns (uint256) { + return _getRoyaltiesType(royaltiesProviders[token]); + } + + /// @notice returns royalties type from uint + /// @param data in uint256 + /// @return royalty type + function _getRoyaltiesType(uint256 data) internal pure returns (uint256) { + for (uint256 i = 1; i <= ROYALTIES_TYPES_AMOUNT; ++i) { + if (data / 2 ** (256 - i) == 1) { + return i; + } + } + return 0; + } + + /// @notice sets royalties type for token contract + /// @param token address of token + /// @param royaltiesType uint256 of royalty type + /// @param royaltiesProvider address of royalty provider + function setRoyaltiesType(address token, uint256 royaltiesType, address royaltiesProvider) internal { + require(royaltiesType > 0 && royaltiesType <= ROYALTIES_TYPES_AMOUNT, "wrong royaltiesType"); + royaltiesProviders[token] = uint(uint160(royaltiesProvider)) + 2 ** (256 - royaltiesType); + } + + /// @notice clears and sets new royalties type for token contract + /// @param token address of token + /// @param royaltiesType roayalty type + function forceSetRoyaltiesType(address token, uint256 royaltiesType) external { + checkOwner(token); + setRoyaltiesType(token, royaltiesType, getProvider(token)); + } + + /// @notice clears royalties type for token contract + /// @param token address of token + function clearRoyaltiesType(address token) external { + checkOwner(token); + royaltiesProviders[token] = uint(uint160(getProvider(token))); + } + + /// @notice sets royalties for token contract in royaltiesByToken mapping and royalties type = 1 + /// @param token address of token + /// @param royalties array of royalties + function setRoyaltiesByToken(address token, LibPart.Part[] memory royalties) external { + checkOwner(token); + //clearing royaltiesProviders value for the token + delete royaltiesProviders[token]; + // setting royaltiesType = 1 for the token + setRoyaltiesType(token, 1, address(0)); + uint256 sumRoyalties = 0; + delete royaltiesByToken[token]; + for (uint256 i = 0; i < royalties.length; ++i) { + require(royalties[i].account != address(0x0), "RoyaltiesByToken recipient should be present"); + require(royalties[i].value != 0, "Royalty value for RoyaltiesByToken should be > 0"); + royaltiesByToken[token].royalties.push(royalties[i]); + sumRoyalties += royalties[i].value; + } + require(sumRoyalties < 10000, "Set by token royalties sum more, than 100%"); + royaltiesByToken[token].initialized = true; + emit RoyaltiesSetForContract(token, royalties); + } + + /// @notice checks if msg.sender is owner of this contract or owner of the token contract + /// @param token address of token + function checkOwner(address token) internal view { + if ((owner() != _msgSender()) && (OwnableUpgradeable(token).owner() != _msgSender())) { + revert("Token owner not detected"); + } + } + + /// @notice calculates royalties type for token contract + /// @param token address of token + /// @param royaltiesProvider address of royalty provider + /// @return royalty type + function calculateRoyaltiesType(address token, address royaltiesProvider) internal view returns (uint256) { + try IERC165Upgradeable(token).supportsInterface(LibRoyalties2981._INTERFACE_ID_ROYALTIES) returns ( + bool result2981 + ) { + if (result2981) { + return 3; + } + // solhint-disable-next-line no-empty-blocks + } catch {} + + if (royaltiesProvider != address(0)) { + return 2; + } + + if (royaltiesByToken[token].initialized) { + return 1; + } + + return 4; + } + + /// @notice returns royalties for token contract and token id + /// @param token address of token + /// @param tokenId id of token + /// @return royalties in form of an array of Parts + function getRoyalties(address token, uint256 tokenId) external override returns (LibPart.Part[] memory) { + uint256 royaltiesProviderData = royaltiesProviders[token]; + + address royaltiesProvider = address(uint160(royaltiesProviderData)); + uint256 royaltiesType = _getRoyaltiesType(royaltiesProviderData); + + // case when royaltiesType is not set + if (royaltiesType == 0) { + // calculating royalties type for token + royaltiesType = calculateRoyaltiesType(token, royaltiesProvider); + + //saving royalties type + setRoyaltiesType(token, royaltiesType, royaltiesProvider); + } + + //case royaltiesType = 1, royalties are set in royaltiesByToken + if (royaltiesType == 1) { + return royaltiesByToken[token].royalties; + } + + //case royaltiesType = 2, royalties from external provider + if (royaltiesType == 2) { + return providerExtractor(token, tokenId, royaltiesProvider); + } + + //case royaltiesType = 3, royalties EIP-2981 + if (royaltiesType == 3) { + return getRoyaltiesEIP2981(token, tokenId); + } + + // case royaltiesType = 4, unknown/empty royalties + if (royaltiesType == 4) { + return new LibPart.Part[](0); + } + + revert("something wrong in getRoyalties"); + } + + /// @notice tries to get royalties EIP-2981 for token and tokenId + /// @param token address of token + /// @param tokenId id of token + /// @return royalties 2981 royalty array + function getRoyaltiesEIP2981( + address token, + uint256 tokenId + ) internal view returns (LibPart.Part[] memory royalties) { + try IERC2981(token).royaltyInfo(tokenId, LibRoyalties2981._WEIGHT_VALUE) returns ( + address receiver, + uint256 royaltyAmount + ) { + try IERC165Upgradeable(token).supportsInterface(INTERFACE_ID_GET_RECIPIENTS) returns (bool result) { + if (result) { + try IMultiRoyaltyRecipients(token).getRecipients(tokenId) returns ( + IMultiRoyaltyRecipients.Recipient[] memory multiRecipients + ) { + uint256 multiRecipientsLength = multiRecipients.length; + royalties = new LibPart.Part[](multiRecipientsLength); + uint256 sum = 0; + for (uint256 i; i < multiRecipientsLength; i++) { + IMultiRoyaltyRecipients.Recipient memory splitRecipient = multiRecipients[i]; + royalties[i].account = splitRecipient.recipient; + uint256 splitAmount = (splitRecipient.bps * royaltyAmount) / LibRoyalties2981._WEIGHT_VALUE; + royalties[i].value = uint96(splitAmount); + sum += splitAmount; + } + // sum can be less than amount, otherwise small-value listings can break + require(sum <= royaltyAmount, "RoyaltiesRegistry: Invalid split"); + return royalties; + } catch { + return LibRoyalties2981.calculateRoyalties(receiver, royaltyAmount); + } + } else { + return LibRoyalties2981.calculateRoyalties(receiver, royaltyAmount); + } + } catch { + return LibRoyalties2981.calculateRoyalties(receiver, royaltyAmount); + } + } catch { + return new LibPart.Part[](0); + } + } + + /// @notice tries to get royalties for token and tokenId from external provider set in royaltiesProviders + /// @param token address of token + /// @param tokenId id of token + /// @param providerAddress address of external provider + /// @return external royalties + function providerExtractor( + address token, + uint256 tokenId, + address providerAddress + ) internal returns (LibPart.Part[] memory) { + try IRoyaltiesProvider(providerAddress).getRoyalties(token, tokenId) returns (LibPart.Part[] memory result) { + return result; + } catch { + return new LibPart.Part[](0); + } + } + + uint256[46] private __gap; +} diff --git a/packages/marketplace/src/royalties-registry/RoyaltyRegistry.md b/packages/marketplace/src/royalties-registry/RoyaltyRegistry.md new file mode 100644 index 0000000000..b7508e5c43 --- /dev/null +++ b/packages/marketplace/src/royalties-registry/RoyaltyRegistry.md @@ -0,0 +1,29 @@ + # Features + + `RoyaltiesRegistry` contract allows to processing different types of royalties: + +* `royaltiesByToken` +* v2 +* v1 +* external provider +* EIP-2981 + +## Methods + +* Sets royalties for the entire collection (`royaltiesByToken`) + + ```javascript + function setRoyaltiesByToken(address token, LibPart.Part[] memory royalties) external + ``` + +* Sets the provider's royalties — a separate contract that will return the royalties + + ```javascript + function setProviderByToken(address token, address provider) external + ``` + +* The implementation of Royalties v2, v1 and EIP-2981 is located inside the token and processed in this method + + ```javascript + function getRoyalties(address token, uint tokenId) + ``` diff --git a/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesProviderTest.sol b/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesProviderTest.sol new file mode 100644 index 0000000000..d33b5be66b --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesProviderTest.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IRoyaltiesProvider} from "../../../interfaces/IRoyaltiesProvider.sol"; +import {LibPart} from "../../../lib-part/LibPart.sol"; + +contract RoyaltiesProviderTest is IRoyaltiesProvider { + mapping(address => mapping(uint256 => LibPart.Part[])) internal royaltiesTest; + + function initializeProvider(address token, uint256 tokenId, LibPart.Part[] memory royalties) public { + delete royaltiesTest[token][tokenId]; + for (uint256 i = 0; i < royalties.length; ++i) { + royaltiesTest[token][tokenId].push(royalties[i]); + } + } + + function getRoyalties(address token, uint256 tokenId) external view override returns (LibPart.Part[] memory) { + return royaltiesTest[token][tokenId]; + } +} diff --git a/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesRegistryOld.sol b/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesRegistryOld.sol new file mode 100644 index 0000000000..e707643ef2 --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesRegistryOld.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IRoyaltiesProvider} from "../../../interfaces/IRoyaltiesProvider.sol"; +import {LibRoyalties2981} from "../../../royalties/LibRoyalties2981.sol"; +import {LibPart} from "../../../lib-part/LibPart.sol"; +import {IERC2981} from "../../../royalties/IERC2981.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +//old RoyaltiesRegistry with royaltiesProviders changed to mapping(address => uint256) to test upgradeability +contract RoyaltiesRegistryOld is IRoyaltiesProvider, OwnableUpgradeable { + event RoyaltiesSetForToken(address indexed token, uint256 indexed tokenId, LibPart.Part[] royalties); + event RoyaltiesSetForContract(address indexed token, LibPart.Part[] royalties); + + struct RoyaltiesSet { + bool initialized; + LibPart.Part[] royalties; + } + + mapping(bytes32 => RoyaltiesSet) public royaltiesByTokenAndTokenId; + mapping(address => RoyaltiesSet) public royaltiesByToken; + mapping(address => uint256) public royaltiesProviders; + + function __RoyaltiesRegistry_init() external initializer { + __Ownable_init(); + } + + function setProviderByToken(address token, address provider) external { + checkOwner(token); + royaltiesProviders[token] = uint160(provider); + } + + function getProvider(address token) public view returns (address) { + return address(uint160(royaltiesProviders[token])); + } + + function setRoyaltiesByToken(address token, LibPart.Part[] memory royalties) external { + checkOwner(token); + uint256 sumRoyalties = 0; + delete royaltiesByToken[token]; + for (uint256 i = 0; i < royalties.length; ++i) { + require(royalties[i].account != address(0x0), "RoyaltiesByToken recipient should be present"); + require(royalties[i].value != 0, "Royalty value for RoyaltiesByToken should be > 0"); + royaltiesByToken[token].royalties.push(royalties[i]); + sumRoyalties += royalties[i].value; + } + require(sumRoyalties < 10000, "Set by token royalties sum more, than 100%"); + royaltiesByToken[token].initialized = true; + emit RoyaltiesSetForContract(token, royalties); + } + + function setRoyaltiesByTokenAndTokenId(address token, uint256 tokenId, LibPart.Part[] memory royalties) external { + checkOwner(token); + setRoyaltiesCacheByTokenAndTokenId(token, tokenId, royalties); + } + + function checkOwner(address token) internal view { + if ((owner() != _msgSender()) && (OwnableUpgradeable(token).owner() != _msgSender())) { + revert("Token owner not detected"); + } + } + + function getRoyalties(address token, uint256 tokenId) external override returns (LibPart.Part[] memory) { + RoyaltiesSet memory royaltiesSet = royaltiesByTokenAndTokenId[keccak256(abi.encode(token, tokenId))]; + if (royaltiesSet.initialized) { + return royaltiesSet.royalties; + } + royaltiesSet = royaltiesByToken[token]; + if (royaltiesSet.initialized) { + return royaltiesSet.royalties; + } + (bool result, LibPart.Part[] memory resultRoyalties) = providerExtractor(token, tokenId); + if (result == false) { + resultRoyalties = royaltiesFromContract(token, tokenId); + } + setRoyaltiesCacheByTokenAndTokenId(token, tokenId, resultRoyalties); + return resultRoyalties; + } + + function setRoyaltiesCacheByTokenAndTokenId( + address token, + uint256 tokenId, + LibPart.Part[] memory royalties + ) internal { + uint256 sumRoyalties = 0; + bytes32 key = keccak256(abi.encode(token, tokenId)); + delete royaltiesByTokenAndTokenId[key].royalties; + for (uint256 i = 0; i < royalties.length; ++i) { + require(royalties[i].account != address(0x0), "RoyaltiesByTokenAndTokenId recipient should be present"); + require(royalties[i].value != 0, "Royalty value for RoyaltiesByTokenAndTokenId should be > 0"); + royaltiesByTokenAndTokenId[key].royalties.push(royalties[i]); + sumRoyalties += royalties[i].value; + } + require(sumRoyalties < 10000, "Set by token and tokenId royalties sum more, than 100%"); + royaltiesByTokenAndTokenId[key].initialized = true; + emit RoyaltiesSetForToken(token, tokenId, royalties); + } + + function royaltiesFromContract(address token, uint256 tokenId) internal view returns (LibPart.Part[] memory) { + (LibPart.Part[] memory royalties, bool isNative) = royaltiesFromContractNative(); + if (isNative) { + return royalties; + } + return royaltiesFromContractSpecial(token, tokenId); + } + + function royaltiesFromContractNative() internal view returns (LibPart.Part[] memory, bool) { + address payable[] memory recipients; + uint256[] memory values; + if (values.length != recipients.length) { + return (new LibPart.Part[](0), true); + } + LibPart.Part[] memory result = new LibPart.Part[](values.length); + for (uint256 i = 0; i < values.length; ++i) { + result[i].value = uint96(values[i]); + result[i].account = recipients[i]; + } + return (result, true); + } + + function royaltiesFromContractSpecial( + address token, + uint256 tokenId + ) internal view returns (LibPart.Part[] memory) { + if (IERC165Upgradeable(token).supportsInterface(LibRoyalties2981._INTERFACE_ID_ROYALTIES)) { + IERC2981 v2981 = IERC2981(token); + try v2981.royaltyInfo(tokenId, LibRoyalties2981._WEIGHT_VALUE) returns ( + address receiver, + uint256 royaltyAmount + ) { + return LibRoyalties2981.calculateRoyalties(receiver, royaltyAmount); + } catch { + return new LibPart.Part[](0); + } + } + return new LibPart.Part[](0); + } + + function providerExtractor( + address token, + uint256 tokenId + ) internal returns (bool result, LibPart.Part[] memory royalties) { + result = false; + address providerAddress = getProvider(token); + if (providerAddress != address(0x0)) { + IRoyaltiesProvider provider = IRoyaltiesProvider(providerAddress); + try provider.getRoyalties(token, tokenId) returns (LibPart.Part[] memory royaltiesByProvider) { + royalties = royaltiesByProvider; + result = true; + // solhint-disable-next-line no-empty-blocks + } catch {} + } + } + + uint256[46] private __gap; +} diff --git a/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesRegistryTest.sol b/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesRegistryTest.sol new file mode 100644 index 0000000000..1d2fdadbcd --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/royalty-registry/RoyaltiesRegistryTest.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IRoyaltiesProvider} from "../../../interfaces/IRoyaltiesProvider.sol"; +import {LibPart} from "../../../lib-part/LibPart.sol"; + +contract RoyaltiesRegistryTest { + event GetRoyaltiesTest(LibPart.Part[] royalties); + + function _getRoyalties(address royaltiesTest, address token, uint256 tokenId) external { + IRoyaltiesProvider withRoyalties = IRoyaltiesProvider(royaltiesTest); + LibPart.Part[] memory royalties = withRoyalties.getRoyalties(token, tokenId); + emit GetRoyaltiesTest(royalties); + } +} diff --git a/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC1155WithRoyaltyV2981.sol b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC1155WithRoyaltyV2981.sol new file mode 100644 index 0000000000..769cbdcc64 --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC1155WithRoyaltyV2981.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Royalties2981TestImpl} from "../../../royalties/mocks/Royalties2981TestImpl.sol"; +import {LibRoyalties2981} from "../../../royalties/LibRoyalties2981.sol"; +import {AbstractRoyalties, LibPart} from "../../../royalties/mocks/AbstractRoyalties.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract TestERC1155WithRoyaltyV2981 is + Initializable, + Royalties2981TestImpl, + AbstractRoyalties, + ERC1155Upgradeable, + OwnableUpgradeable +{ + uint256 internal constant BASIS_POINTS = 10000; + + function initialize() public initializer { + __Ownable_init(); + } + + function mint(address to, uint256 tokenId, uint256 amount, LibPart.Part[] memory _fees) external { + _mint(to, tokenId, amount, ""); + _saveRoyalties(tokenId, _fees); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == LibRoyalties2981._INTERFACE_ID_ROYALTIES; + } + + // solhint-disable-next-line no-empty-blocks + function _onRoyaltiesSet(uint256 _id, LibPart.Part[] memory _fees) internal override {} +} diff --git a/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721.sol b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721.sol new file mode 100644 index 0000000000..e06ae21808 --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC721} from "../../../test/TestERC721.sol"; diff --git a/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981.sol b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981.sol new file mode 100644 index 0000000000..e4a1b9b647 --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Royalties2981TestImpl} from "../../../royalties/mocks/Royalties2981TestImpl.sol"; +import {LibRoyalties2981} from "../../../royalties/LibRoyalties2981.sol"; +import {AbstractRoyalties, LibPart} from "../../../royalties/mocks/AbstractRoyalties.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract TestERC721WithRoyaltyV2981 is + Initializable, + Royalties2981TestImpl, + AbstractRoyalties, + ERC721Upgradeable, + OwnableUpgradeable +{ + function initialize() public initializer { + __Ownable_init(); + } + + function mint(address to, uint256 tokenId, LibPart.Part[] memory _fees) external { + _mint(to, tokenId); + _saveRoyalties(tokenId, _fees); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == LibRoyalties2981._INTERFACE_ID_ROYALTIES; + } + + // solhint-disable-next-line no-empty-blocks + function _onRoyaltiesSet(uint256 _id, LibPart.Part[] memory _fees) internal override {} +} diff --git a/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981Multi.sol b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981Multi.sol new file mode 100644 index 0000000000..65b26f2a0d --- /dev/null +++ b/packages/marketplace/src/royalties-registry/mocks/tokens/TestERC721WithRoyaltyV2981Multi.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Royalties2981TestImpl} from "../../../royalties/mocks/Royalties2981TestImpl.sol"; +import {LibRoyalties2981} from "../../../royalties/LibRoyalties2981.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract TestERC721WithRoyaltyV2981Multi is + Initializable, + Royalties2981TestImpl, + ERC721Upgradeable, + OwnableUpgradeable +{ + uint256 internal constant BASIS_POINTS = 10000; + + bytes4 internal constant INTERFACE_ID_IROYALTYUGC = 0xa30b4db9; + + struct Recipient { + address payable recipient; + uint16 bps; + } + + Recipient[] private _recipients; + + function initialize() public initializer { + __Ownable_init(); + setRoyalties(5000); + } + + function mint(address to, uint256 tokenId, Recipient[] memory _fees) external { + _mint(to, tokenId); + _setRecipients(_fees); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == LibRoyalties2981._INTERFACE_ID_ROYALTIES || interfaceId == INTERFACE_ID_IROYALTYUGC; + } + + function _setRecipients(Recipient[] memory recipients) internal { + delete _recipients; + if (recipients.length == 0) { + return; + } + uint256 totalBPS; + for (uint256 i; i < recipients.length; ++i) { + totalBPS += recipients[i].bps; + _recipients.push(recipients[i]); + } + require(totalBPS == BASIS_POINTS, "Total bps must be 10000"); + } + + function getRecipients() external view returns (Recipient[] memory) { + return _recipients; + } + + function getCreatorAddress(uint256) external view returns (address creator) { + // creator = address(uint160(tokenId)); + // return creator; + return owner(); + } +} diff --git a/packages/marketplace/src/royalties/IERC2981.sol b/packages/marketplace/src/royalties/IERC2981.sol new file mode 100644 index 0000000000..9608b2c5f8 --- /dev/null +++ b/packages/marketplace/src/royalties/IERC2981.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title Interface for ERC2981 +/// @notice NFT Royalty Standard +interface IERC2981 { + /// @notice Called with the sale price to determine how much royalty is owed and to whom. + /// @param _tokenId - the NFT asset queried for royalty information + /// @param _salePrice - the sale price of the NFT asset specified by _tokenId + /// @return receiver - address of who should be sent the royalty payment + /// @return royaltyAmount - the royalty payment amount for _salePrice + function royaltyInfo( + uint256 _tokenId, + uint256 _salePrice + ) external view returns (address receiver, uint256 royaltyAmount); +} diff --git a/packages/marketplace/src/royalties/LibRoyalties2981.sol b/packages/marketplace/src/royalties/LibRoyalties2981.sol new file mode 100644 index 0000000000..392eb78c49 --- /dev/null +++ b/packages/marketplace/src/royalties/LibRoyalties2981.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibPart} from "../lib-part/LibPart.sol"; + +/// @title library for constants and functions related to ERC2891 +/// @notice standard for signature validation +library LibRoyalties2981 { + bytes4 public constant _INTERFACE_ID_ROYALTIES = 0x2a55205a; + uint96 internal constant _WEIGHT_VALUE = 1e6; + + /// @notice method for converting amount to percent and forming LibPart + /// @param to recipient of royalties + /// @param amount of royalties + /// @return LibPart with account and value + function calculateRoyalties(address to, uint256 amount) internal pure returns (LibPart.Part[] memory) { + LibPart.Part[] memory result; + if (amount == 0) { + return result; + } + uint256 percent = (amount * 10000) / _WEIGHT_VALUE; + require(percent < 10000, "Royalties 2981 exceeds 100%"); + result = new LibPart.Part[](1); + result[0].account = payable(to); + result[0].value = uint96(percent); + return result; + } +} diff --git a/packages/marketplace/src/royalties/mocks/AbstractRoyalties.sol b/packages/marketplace/src/royalties/mocks/AbstractRoyalties.sol new file mode 100644 index 0000000000..7ed82df806 --- /dev/null +++ b/packages/marketplace/src/royalties/mocks/AbstractRoyalties.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibPart} from "../../lib-part/LibPart.sol"; + +abstract contract AbstractRoyalties { + mapping(uint256 => LibPart.Part[]) internal royalties; + + function _saveRoyalties(uint256 id, LibPart.Part[] memory _royalties) internal { + uint256 totalValue; + for (uint256 i = 0; i < _royalties.length; ++i) { + require(_royalties[i].account != address(0x0), "Recipient should be present"); + require(_royalties[i].value != 0, "Royalty value should be positive"); + totalValue += _royalties[i].value; + royalties[id].push(_royalties[i]); + } + require(totalValue < 10000, "Royalty total value should be < 10000"); + _onRoyaltiesSet(id, _royalties); + } + + function _updateAccount(uint256 _id, address _from, address _to) internal { + uint256 length = royalties[_id].length; + for (uint256 i = 0; i < length; ++i) { + if (royalties[_id][i].account == _from) { + royalties[_id][i].account = payable(address(uint160(_to))); + } + } + } + + function _onRoyaltiesSet(uint256 id, LibPart.Part[] memory _royalties) internal virtual; +} diff --git a/packages/marketplace/src/royalties/mocks/Royalties2981Test.sol b/packages/marketplace/src/royalties/mocks/Royalties2981Test.sol new file mode 100644 index 0000000000..bc671a4785 --- /dev/null +++ b/packages/marketplace/src/royalties/mocks/Royalties2981Test.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC2981} from "../IERC2981.sol"; + +contract Royalties2981Test { + IERC2981 internal immutable ROYALTIES; + + constructor(IERC2981 _royalties) { + ROYALTIES = _royalties; + } + + event Test(address account, uint256 value); + + function royaltyInfoTest(uint256 _tokenId, uint256 _salePrice) public { + (address account, uint256 value) = ROYALTIES.royaltyInfo(_tokenId, _salePrice); + emit Test(account, value); + } +} diff --git a/packages/marketplace/src/royalties/mocks/Royalties2981TestImpl.sol b/packages/marketplace/src/royalties/mocks/Royalties2981TestImpl.sol new file mode 100644 index 0000000000..6d330d81e5 --- /dev/null +++ b/packages/marketplace/src/royalties/mocks/Royalties2981TestImpl.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {IERC2981} from "../IERC2981.sol"; +import {LibRoyalties2981} from "../LibRoyalties2981.sol"; +import {LibPart} from "../../lib-part/LibPart.sol"; + +contract Royalties2981TestImpl is IERC2981 { + uint256 public royaltiesBasePoint; + + function setRoyalties(uint256 _value) public { + royaltiesBasePoint = _value; + } + + function royaltyInfo( + uint256 _tokenId, + uint256 _salePrice + ) external view override returns (address receiver, uint256 royaltyAmount) { + receiver = address(uint160(_tokenId >> 96)); + royaltyAmount = (_salePrice * royaltiesBasePoint) / 10000; + } + + function calculateRoyaltiesTest(address payable to, uint96 amount) external pure returns (LibPart.Part[] memory) { + return LibRoyalties2981.calculateRoyalties(to, amount); + } +} diff --git a/packages/marketplace/src/test/TestChainId.sol b/packages/marketplace/src/test/TestChainId.sol new file mode 100644 index 0000000000..93c696f7a5 --- /dev/null +++ b/packages/marketplace/src/test/TestChainId.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +contract TestChainId { + function getChainID() public view returns (uint256 id) { + assembly { + id := chainid() + } + } +} diff --git a/packages/marketplace/src/test/TestERC1155.sol b/packages/marketplace/src/test/TestERC1155.sol new file mode 100644 index 0000000000..acc188c2f1 --- /dev/null +++ b/packages/marketplace/src/test/TestERC1155.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; + +contract TestERC1155 is ERC1155Upgradeable { + function mint(address to, uint256 tokenId, uint256 amount) external { + _mint(to, tokenId, amount, ""); + } +} diff --git a/packages/marketplace/src/test/TestERC20.sol b/packages/marketplace/src/test/TestERC20.sol new file mode 100644 index 0000000000..a5469ce60c --- /dev/null +++ b/packages/marketplace/src/test/TestERC20.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +contract TestERC20 is ERC20Upgradeable { + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/packages/marketplace/src/test/TestERC20ZRX.sol b/packages/marketplace/src/test/TestERC20ZRX.sol new file mode 100644 index 0000000000..6b5471bc47 --- /dev/null +++ b/packages/marketplace/src/test/TestERC20ZRX.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT + +/** + *Submitted for verification at Etherscan.io on 2017-08-11 + */ + +/* + + Copyright 2017 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +// solhint-disable-next-line one-contract-per-file +pragma solidity 0.8.19; + +interface Token { + /// @return total amount of tokens + function totalSupply() external view returns (uint256); + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) external view returns (uint256); + + /// @notice send `_value` token to `_to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _value) external returns (bool); + + /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) external returns (bool); + + /// @notice `msg.sender` approves `_addr` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _value) external returns (bool); + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent + function allowance(address _owner, address _spender) external view returns (uint256); + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); +} + +abstract contract StandardToken is Token { + function transfer(address _to, uint256 _value) public override returns (bool success) { + //Default assumes totalSupply can't be over max (2^256 - 1). + if (balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) { + balances[msg.sender] -= _value; + balances[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; + } else { + return false; + } + } + + function transferFrom(address _from, address _to, uint256 _value) public virtual override returns (bool success) { + if ( + balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value >= balances[_to] + ) { + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + emit Transfer(_from, _to, _value); + return true; + } else { + return false; + } + } + + function balanceOf(address _owner) public view override returns (uint256 balance) { + return balances[_owner]; + } + + function approve(address _spender, uint256 _value) public override returns (bool success) { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + function allowance(address _owner, address _spender) public view override returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + mapping(address => uint256) internal balances; + mapping(address => mapping(address => uint256)) internal allowed; +} + +abstract contract UnlimitedAllowanceToken is StandardToken { + uint256 internal constant MAX_UINT = 2 ** 256 - 1; + + /// @dev ERC20 transferFrom, modified such that an allowance of MAX_UINT represents an unlimited allowance. + /// @param _from Address to transfer from. + /// @param _to Address to transfer to. + /// @param _value Amount to transfer. + /// @return Success of transfer. + function transferFrom(address _from, address _to, uint256 _value) public override returns (bool) { + uint256 allowance = allowed[_from][msg.sender]; + if (balances[_from] >= _value && allowance >= _value && balances[_to] + _value >= balances[_to]) { + balances[_to] += _value; + balances[_from] -= _value; + if (allowance < MAX_UINT) { + allowed[_from][msg.sender] -= _value; + } + emit Transfer(_from, _to, _value); + return true; + } else { + return false; + } + } +} + +contract TestERC20ZRX is UnlimitedAllowanceToken { + uint256 private _totalSupply = 10 ** 27; // 1 billion tokens, 18 decimal places + string private _name = "0x Protocol Token"; + string private _symbol = "ZRX"; + + constructor() { + balances[msg.sender] = _totalSupply; + } + + function name() public view returns (string memory) { + return _name; + } + + function symbol() public view returns (string memory) { + return _symbol; + } + + function decimals() public pure returns (uint8) { + return 18; + } + + function totalSupply() external view override returns (uint256) { + return _totalSupply; + } +} diff --git a/packages/marketplace/src/test/TestERC721.sol b/packages/marketplace/src/test/TestERC721.sol new file mode 100644 index 0000000000..04630d8460 --- /dev/null +++ b/packages/marketplace/src/test/TestERC721.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + +contract TestERC721 is ERC721Upgradeable { + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } +} diff --git a/packages/marketplace/src/transfer-manager/Migrations.sol b/packages/marketplace/src/transfer-manager/Migrations.sol new file mode 100644 index 0000000000..45bff221ac --- /dev/null +++ b/packages/marketplace/src/transfer-manager/Migrations.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +contract Migrations { + address public owner = msg.sender; + uint256 public lastCompletedMigration; + + modifier restricted() { + require(msg.sender == owner, "This function is restricted to the contract's owner"); + _; + } + + function setCompleted(uint256 completed) public restricted { + lastCompletedMigration = completed; + } +} diff --git a/packages/marketplace/src/transfer-manager/TransferExecutor.md b/packages/marketplace/src/transfer-manager/TransferExecutor.md new file mode 100644 index 0000000000..d1d6e9b3f5 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/TransferExecutor.md @@ -0,0 +1,10 @@ +#### Features + +`TransferExecutor.transfer` function should be able to transfer any supported Asset (see more at [LibAsset](../lib-asset/LibAsset.md)) from one side of the order to the other side of the order. + +Transfer is made using different types of contracts. There are 3 types of transfer proxies used by `TransferExecutor`: +- INftTransferProxy - this proxy is used to transfer NFTs (ERC-721 and ERC-1155) +- IERC20TransferProxy - this proxy is used to transfer ERC20 tokens +- ITransferProxy - this is generic proxy used to transfer all other types of Assets + +`TransferExecutor` has setTransferProxy to register new types of transfer proxies (when new types of Assets gets registered) diff --git a/packages/marketplace/src/transfer-manager/TransferExecutor.sol b/packages/marketplace/src/transfer-manager/TransferExecutor.sol new file mode 100644 index 0000000000..e03d7fde70 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/TransferExecutor.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibERC1155LazyMint} from "../lazy-mint/erc-1155/LibERC1155LazyMint.sol"; +import {IERC1155LazyMint} from "../lazy-mint/erc-1155/IERC1155LazyMint.sol"; +import {LibERC721LazyMint} from "../lazy-mint/erc-721/LibERC721LazyMint.sol"; +import {IERC721LazyMint} from "../lazy-mint/erc-721/IERC721LazyMint.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {IERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; +import {ITransferExecutor} from "./interfaces/ITransferExecutor.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {LibTransfer} from "./lib/LibTransfer.sol"; +import {LibAsset} from "../lib-asset/LibAsset.sol"; + +/// @title abstract contract for TransferExecutor +/// @notice contains transfer functions for any assets as well as ERC20 tokens +abstract contract TransferExecutor is Initializable, OwnableUpgradeable, ITransferExecutor { + using LibTransfer for address payable; + + // Bundle Structs + struct ERC20Details { + IERC20Upgradeable token; + uint256 value; + } + + struct ERC721Details { + IERC721Upgradeable token; + uint256 id; + uint256 value; + } + + struct ERC1155Details { + IERC1155Upgradeable token; + uint256 id; + uint256 value; + // bytes data; + } + + /// @notice function should be able to transfer any supported Asset + /// @param asset Asset to be transferred + /// @param from account holding the asset + /// @param to account that will receive the asset + function transfer(LibAsset.Asset memory asset, address from, address to) internal override { + if (asset.assetType.assetClass == LibAsset.ERC721_ASSET_CLASS) { + //not using transfer proxy when transferring from this contract + (address token, uint256 tokenId) = abi.decode(asset.assetType.data, (address, uint256)); + require(asset.value == 1, "erc721 value error"); + if (from == address(this)) { + IERC721Upgradeable(token).safeTransferFrom(address(this), to, tokenId); + } else { + erc721safeTransferFrom(IERC721Upgradeable(token), from, to, tokenId); + } + } else if (asset.assetType.assetClass == LibAsset.ERC20_ASSET_CLASS) { + //not using transfer proxy when transferring from this contract + address token = abi.decode(asset.assetType.data, (address)); + if (from == address(this)) { + require(IERC20Upgradeable(token).transfer(to, asset.value), "erc20 transfer failed"); + } else { + erc20safeTransferFrom(IERC20Upgradeable(token), from, to, asset.value); + } + } else if (asset.assetType.assetClass == LibAsset.ERC1155_ASSET_CLASS) { + //not using transfer proxy when transferring from this contract + (address token, uint256 tokenId) = abi.decode(asset.assetType.data, (address, uint256)); + if (from == address(this)) { + IERC1155Upgradeable(token).safeTransferFrom(address(this), to, tokenId, asset.value, ""); + } else { + erc1155safeTransferFrom(IERC1155Upgradeable(token), from, to, tokenId, asset.value, ""); + } + } else if (asset.assetType.assetClass == LibAsset.ETH_ASSET_CLASS) { + if (to != address(this)) { + payable(to).transferEth(asset.value); + } + } else if (asset.assetType.assetClass == LibAsset.BUNDLE) { + // unpack the asset data + ERC20Details[] memory erc20Details; + ERC721Details[] memory erc721Details; + ERC1155Details[] memory erc1155Details; + + (erc20Details, erc721Details, erc1155Details) = abi.decode( + asset.assetType.data, + (ERC20Details[], ERC721Details[], ERC1155Details[]) + ); + // TODO: Limit the max number of bundle assets to avoid issues + // transfer ERC20 + for (uint256 i; i < erc20Details.length; ) { + if (from == address(this)) { + require( + erc20Details[i].token.transferFrom(from, to, erc20Details[i].value), + "erc20 bundle transfer failed" + ); + } else { + erc20safeTransferFrom(erc20Details[i].token, from, to, erc20Details[i].value); + } + + unchecked { + ++i; + } + } + + // transfer ERC721 assets + for (uint256 i; i < erc721Details.length; ) { + require(erc721Details[i].value == 1, "erc721 value error"); + if (from == address(this)) { + erc721Details[i].token.safeTransferFrom(address(this), to, erc721Details[i].id); + } else { + erc721safeTransferFrom(erc721Details[i].token, from, to, erc721Details[i].id); + } + + unchecked { + ++i; + } + } + + // transfer ERC1155 assets + for (uint256 i; i < erc1155Details.length; ) { + if (from == address(this)) { + erc1155Details[i].token.safeTransferFrom( + address(this), + to, + erc1155Details[i].id, + erc1155Details[i].value, + "" + ); + } else { + erc1155safeTransferFrom( + erc1155Details[i].token, + from, + to, + erc1155Details[i].id, + erc1155Details[i].value, + "" + ); + } + + unchecked { + ++i; + } + } + } else { + lazyTransfer(asset, from, to); + } + } + + /// @notice function for safe transfer of ERC20 tokens + /// @param token ERC20 token to be transferred + /// @param from address from which tokens will be taken + /// @param to address that will receive tokens + /// @param value how many tokens are going to be transferred + function erc20safeTransferFrom(IERC20Upgradeable token, address from, address to, uint256 value) internal { + require(token.transferFrom(from, to, value), "failure while transferring"); + } + + /// @notice function for safe transfer of ERC721 tokens + /// @param token ERC721 token to be transferred + /// @param from address from which token will be taken + /// @param to address that will receive token + /// @param tokenId id of the token being transferred + function erc721safeTransferFrom(IERC721Upgradeable token, address from, address to, uint256 tokenId) internal { + token.safeTransferFrom(from, to, tokenId); + } + + /// @notice function for safe transfer of ERC1155 tokens + /// @param token ERC1155 token to be transferred + /// @param from address from which tokens will be taken + /// @param to address that will receive tokens + /// @param id id of the tokens being transferred + /// @param value how many tokens will be transferred + function erc1155safeTransferFrom( + IERC1155Upgradeable token, + address from, + address to, + uint256 id, + uint256 value, + bytes memory data + ) internal { + token.safeTransferFrom(from, to, id, value, data); + } + + /// @notice function for lazy transfer + /// @param asset asset that will be lazy transferred + /// @param from address that minted token + /// @param to address that will receive tokens + function lazyTransfer(LibAsset.Asset memory asset, address from, address to) internal { + if (asset.assetType.assetClass == LibERC721LazyMint.ERC721_LAZY_ASSET_CLASS) { + require(asset.value == 1, "erc721 value error"); + (address token, LibERC721LazyMint.Mint721Data memory data) = abi.decode( + asset.assetType.data, + (address, LibERC721LazyMint.Mint721Data) + ); + IERC721LazyMint(token).transferFromOrMint(data, from, to); + } else if (asset.assetType.assetClass == LibERC1155LazyMint.ERC1155_LAZY_ASSET_CLASS) { + (address token, LibERC1155LazyMint.Mint1155Data memory data) = abi.decode( + asset.assetType.data, + (address, LibERC1155LazyMint.Mint1155Data) + ); + IERC1155LazyMint(token).transferFromOrMint(data, from, to, asset.value); + } + } + + uint256[49] private __gap; +} diff --git a/packages/marketplace/src/transfer-manager/TransferManager.md b/packages/marketplace/src/transfer-manager/TransferManager.md new file mode 100644 index 0000000000..7df10aa053 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/TransferManager.md @@ -0,0 +1,77 @@ +### Features + +[TransferManager](TransferManager.sol) is [ITransferManager](./interfaces/ITransferManager.sol). +It's responsible for transferring all Assets. This manager supports different types of fees, also it supports different beneficiaries (specified in Order.data) + +Types of fees supported: +- (!DEPRECATED) protocol fee (controlled by `protocolFee` field) +- origin fee (is coming from `Order.data`) +- royalties (provided by external `IRoyaltiesProvider`) + +### Algorithm +The transferring of assets takes places inside `doTransfers`, it takes following parameters as arguments: +- `LibAsset.AssetType` `makeMatch` - `AssetType` of a make-side asset of the order +- `LibAsset.AssetType` `takeMatch` - `AssetType` of a take-side asset of the order +- `LibFill.FillResult` `fill` - values from both sides to be transferred by this match +- `LibOrder.Order` `leftOrder` - left order data +- `LibOrder.Order` `rightOrder` - right order data + +Then, in this method the following actions are done: + +1. At first, fee side of the deal is calculated (Asset that can be interpreted as money). All fees and royalties will be taken in that Asset. + - to do so, we use `LibFeeSide.getFeeSide`, it takes assetClasses of both sides as arguments (e.g. "`ETH`" and "`ERC20`") and tries to determine which side is the fee side + - firstly it checks both assets for being `ETH`, if assetClass of any side is `ETH` then that side is the fee-side + - if there is no `ETH` in this match, both sides are checked for being `ERC20` + - then both sides are checked for being `ERC1155` + - if there are no `ETH`, `ERC20` or `ERC1155` in this match, then the fee side is `NONE` + - checks are made from make to take side, e.g. if both sides are `ERC20` then the make side is the fee side + +2. then transfers are made: + - if fee side is make: + - `doTransfersWithFees` is called for the make side, + - `transferPayouts` for the take side + - fee side is take + - `doTransfersWithFees` is called for the take side, + - `transferPayouts` for the make side + - fee side is NONE: + - `transferPayouts` is called for both sides + +- `doTransfersWithFees` + - calculates total amount of asset that is going to be transferred + - then royalties are transferred + - royalty is a fee that transferred to creator of `ERC1155`/`ERC721` token from every purchase + - royalties can't be over 50 % + - after that, origin fees are transferred + - origin fees can be added to any order, it's an array of address + value + - finally, `transferPayouts` is executed as the final action of `doTransfersWithFees` + +![Fees](../../exchange-v2/images/fees.svg) + +- `transferPayouts` + - transfers assets to payout address + - orders can have any number of payout addresses, e.g. payouts can be split 50/50 between 2 accounts, or any other way + - payuots are set in order by order maker + - if `transferPayouts` called in the end of `doTransfersWithFees`, then it transfers all that's left after paying fees + - if `transferPayouts` called for the nft side (non fee side), then it transfers full amount of this asset + + +So, to sum it all up, lets try to calculate all fees for a simple example (for V1 and V2 orders) +- there are 2 orders + 1. `ETH 100` => `1 ERC721`, `orderMaker` = acc1, `origins` = [[acc2, 3%], [acc3, 10%]], `payouts` = [[acc1, 100%]] + 2. `1 ERC721` => `100 ETH`, `orderMaker` = acc4, `origins` = [[]], `payouts` = [[acc4, 70%], [acc5, 30%]] +- royalty is set to 30% +- these are new orders, so they don't have previous fills +- fee side here is ETH, so we do + - `transferPayouts`(1 ERC721) + - there is only one payout address and 1 token, so it simply gets transferred to acc1 + - `doTransfersWithFees`(100 ETH) + - let's calculate ETH amount to be sent by acc1 + - 100 ETH + 3% acc2-origin + 10% acc3-origin = 100 + 0,13*100 = 113 ETH + - so acc1 needs to send 113 ETH, from which 3 are sent to acc2 as origin, 10 are sent to acc3 as origin too ( 100 ETH left) + - 30 ETH paid as royalty to nft creator, 70 left + - right order doesn't have origins, so we skip it + - what's left is divided between acc4 and acc5, as it says in right order payouts (`payouts` = [[acc4, 70%], [acc5, 30%]]), so 49 ETH goes to acc4, 21 ETH to acc5 + + + + diff --git a/packages/marketplace/src/transfer-manager/TransferManager.sol b/packages/marketplace/src/transfer-manager/TransferManager.sol new file mode 100644 index 0000000000..ed0704cc77 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/TransferManager.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ERC165Upgradeable, IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {LibERC721LazyMint} from "../lazy-mint/erc-721/LibERC721LazyMint.sol"; +import {LibERC1155LazyMint} from "../lazy-mint/erc-1155/LibERC1155LazyMint.sol"; +import {IRoyaltiesProvider} from "../interfaces/IRoyaltiesProvider.sol"; +import {BpLibrary} from "../lib-bp/BpLibrary.sol"; +import {IRoyaltyUGC} from "./interfaces/IRoyaltyUGC.sol"; +import {ITransferManager, LibDeal} from "./interfaces/ITransferManager.sol"; +import {LibAsset} from "../lib-asset/LibAsset.sol"; +import {LibPart} from "../lib-part/LibPart.sol"; +import {LibFeeSide} from "./lib/LibFeeSide.sol"; + +/// @title TransferManager contract +/// @notice responsible for transferring all Assets +/// @dev this manager supports different types of fees +/// @dev also it supports different beneficiaries +abstract contract TransferManager is OwnableUpgradeable, ITransferManager, ERC165Upgradeable { + using BpLibrary for uint; + + bytes4 internal constant INTERFACE_ID_IROYALTYUGC = 0xa30b4db9; + + /// @notice fee for primary sales + /// @return uint256 of primary sale fee + uint256 public protocolFeePrimary; + + /// @notice fee for secondary sales + /// @return uint256 of secondary sale fee + uint256 public protocolFeeSecondary; + + /// @notice Registry for the different royalties + /// @return address of royaltiesRegistry + IRoyaltiesProvider public royaltiesRegistry; + + address private defaultFeeReceiver; + + /// @notice event for when protocol fees are set + /// @param newProtocolFeePrimary fee for primary market + /// @param newProtocolFeeSecondary fee for secondary market + event ProtocolFeeSetted(uint256 newProtocolFeePrimary, uint256 newProtocolFeeSecondary); + + /// @notice event for when a royalties registry is set + /// @param newRoyaltiesRegistry address of new royalties registry + event RoyaltiesRegistrySetted(IRoyaltiesProvider newRoyaltiesRegistry); + + /// @notice initializer for TransferExecutor + /// @param newProtocolFeePrimary fee for primary market + /// @param newProtocolFeeSecondary fee for secondary market + /// @param newDefaultFeeReceiver address for account receiving fees + /// @param newRoyaltiesProvider address of royalties registry + function __TransferManager_init_unchained( + uint256 newProtocolFeePrimary, + uint256 newProtocolFeeSecondary, + address newDefaultFeeReceiver, + IRoyaltiesProvider newRoyaltiesProvider + ) internal { + protocolFeePrimary = newProtocolFeePrimary; + protocolFeeSecondary = newProtocolFeeSecondary; + defaultFeeReceiver = newDefaultFeeReceiver; + royaltiesRegistry = newRoyaltiesProvider; + } + + /// @notice setter for royalty registry + /// @param newRoyaltiesRegistry address of new royalties registry + function setRoyaltiesRegistry(IRoyaltiesProvider newRoyaltiesRegistry) external onlyOwner { + royaltiesRegistry = newRoyaltiesRegistry; + + emit RoyaltiesRegistrySetted(newRoyaltiesRegistry); + } + + /// @notice setter for protocol fees + /// @param newProtocolFeePrimary fee for primary market + /// @param newProtocolFeeSecondary fee for secondary market + function setProtocolFee(uint256 newProtocolFeePrimary, uint256 newProtocolFeeSecondary) external onlyOwner { + require(newProtocolFeePrimary < 5000, "invalid primary fee"); + require(newProtocolFeeSecondary < 5000, "invalid secodary fee"); + protocolFeePrimary = newProtocolFeePrimary; + protocolFeeSecondary = newProtocolFeeSecondary; + + emit ProtocolFeeSetted(newProtocolFeePrimary, newProtocolFeeSecondary); + } + + /// @notice executes transfers for 2 matched orders + /// @param left DealSide from the left order (see LibDeal.sol) + /// @param right DealSide from the right order (see LibDeal.sol) + /// @param dealData DealData of the match (see LibDeal.sol) + /// @return totalLeftValue - total amount for the left order + /// @return totalRightValue - total amount for the right order + function doTransfers( + LibDeal.DealSide memory left, + LibDeal.DealSide memory right, + LibDeal.DealData memory dealData + ) internal override returns (uint256 totalLeftValue, uint256 totalRightValue) { + totalLeftValue = left.asset.value; + totalRightValue = right.asset.value; + if (dealData.feeSide == LibFeeSide.FeeSide.LEFT) { + totalLeftValue = doTransfersWithFees(left, right, dealData.maxFeesBasePoint); + transferPayouts(right.asset.assetType, right.asset.value, right.from, left.payouts); + } else if (dealData.feeSide == LibFeeSide.FeeSide.RIGHT) { + totalRightValue = doTransfersWithFees(right, left, dealData.maxFeesBasePoint); + transferPayouts(left.asset.assetType, left.asset.value, left.from, right.payouts); + } else { + transferPayouts(left.asset.assetType, left.asset.value, left.from, right.payouts); + transferPayouts(right.asset.assetType, right.asset.value, right.from, left.payouts); + } + } + + /// @notice executes the fee-side transfers (payment + fees) + /// @param paymentSide DealSide of the fee-side order + /// @param nftSide DealSide of the nft-side order + /// @param maxFeesBasePoint max fee for the sell-order (used and is > 0 for V3 orders only) + /// @return totalAmount of fee-side asset + function doTransfersWithFees( + LibDeal.DealSide memory paymentSide, + LibDeal.DealSide memory nftSide, + uint256 maxFeesBasePoint + ) internal returns (uint256 totalAmount) { + totalAmount = calculateTotalAmount(paymentSide.asset.value, paymentSide.originFees, maxFeesBasePoint); + uint256 rest = totalAmount; + + rest = transferRoyalties( + paymentSide.asset.assetType, + nftSide.asset.assetType, + nftSide.payouts, + rest, + paymentSide.asset.value, + paymentSide.from + ); + + LibPart.Part[] memory origin = new LibPart.Part[](1); + origin[0].account = payable(defaultFeeReceiver); + + bool primaryMarket = false; + + // check if primary or secondary market + if ( + nftSide.asset.assetType.assetClass == LibAsset.ERC1155_ASSET_CLASS || + nftSide.asset.assetType.assetClass == LibAsset.ERC721_ASSET_CLASS + ) { + (address token, uint256 tokenId) = abi.decode(nftSide.asset.assetType.data, (address, uint)); + try IERC165Upgradeable(token).supportsInterface(INTERFACE_ID_IROYALTYUGC) returns (bool result) { + if (result) { + address creator = IRoyaltyUGC(token).getCreatorAddress(tokenId); + if (nftSide.from == creator) { + primaryMarket = true; + } + } + // solhint-disable-next-line no-empty-blocks + } catch {} + } + if (primaryMarket) { + origin[0].value = uint96(protocolFeePrimary); + } else { + origin[0].value = uint96(protocolFeeSecondary); + } + + (rest, ) = transferFees(paymentSide.asset.assetType, rest, paymentSide.asset.value, origin, paymentSide.from); + + transferPayouts(paymentSide.asset.assetType, rest, paymentSide.from, nftSide.payouts); + } + + /// @notice transfer royalties. If there is only one royalties receiver and one address in payouts and they match, + /// @dev nothing is transferred in this function + /// @param paymentAssetType Asset Type which represents payment + /// @param nftAssetType Asset Type which represents NFT to pay royalties for + /// @param payouts Payouts to be made + /// @param rest How much of the amount left after previous transfers + /// @param amount total amount of asset that is going to be transferred + /// @param from owner of the Asset to transfer + /// @return How much left after transferring royalties + function transferRoyalties( + LibAsset.AssetType memory paymentAssetType, + LibAsset.AssetType memory nftAssetType, + LibPart.Part[] memory payouts, + uint256 rest, + uint256 amount, + address from + ) internal returns (uint256) { + LibPart.Part[] memory royalties = getRoyaltiesByAssetType(nftAssetType); + + if ( + nftAssetType.assetClass == LibAsset.ERC1155_ASSET_CLASS || + nftAssetType.assetClass == LibAsset.ERC721_ASSET_CLASS + ) { + (address token, uint256 tokenId) = abi.decode(nftAssetType.data, (address, uint)); + try IERC165Upgradeable(token).supportsInterface(INTERFACE_ID_IROYALTYUGC) returns (bool resultInterface) { + if (resultInterface) { + address creator = IRoyaltyUGC(token).getCreatorAddress(tokenId); + if (payouts.length == 1 && payouts[0].account == creator) { + require(royalties[0].value <= 5000, "Royalties are too high (>50%)"); + return rest; + } + } + // solhint-disable-next-line no-empty-blocks + } catch {} + } + if (royalties.length == 1 && payouts.length == 1 && royalties[0].account == payouts[0].account) { + require(royalties[0].value <= 5000, "Royalties are too high (>50%)"); + return rest; + } + + (uint256 result, uint256 totalRoyalties) = transferFees(paymentAssetType, rest, amount, royalties, from); + require(totalRoyalties <= 5000, "Royalties are too high (>50%)"); + return result; + } + + /// @notice calculates royalties by asset type. If it's a lazy NFT, then royalties are extracted from asset. otherwise using royaltiesRegistry + /// @param nftAssetType NFT Asset Type to calculate royalties for + /// @return calculated royalties (Array of LibPart.Part) + function getRoyaltiesByAssetType(LibAsset.AssetType memory nftAssetType) internal returns (LibPart.Part[] memory) { + if ( + nftAssetType.assetClass == LibAsset.ERC1155_ASSET_CLASS || + nftAssetType.assetClass == LibAsset.ERC721_ASSET_CLASS + ) { + (address token, uint256 tokenId) = abi.decode(nftAssetType.data, (address, uint)); + return royaltiesRegistry.getRoyalties(token, tokenId); + } else if (nftAssetType.assetClass == LibERC1155LazyMint.ERC1155_LAZY_ASSET_CLASS) { + (, LibERC1155LazyMint.Mint1155Data memory data) = abi.decode( + nftAssetType.data, + (address, LibERC1155LazyMint.Mint1155Data) + ); + return data.royalties; + } else if (nftAssetType.assetClass == LibERC721LazyMint.ERC721_LAZY_ASSET_CLASS) { + (, LibERC721LazyMint.Mint721Data memory data) = abi.decode( + nftAssetType.data, + (address, LibERC721LazyMint.Mint721Data) + ); + return data.royalties; + } + LibPart.Part[] memory empty; + return empty; + } + + /// @notice Transfer fees + /// @param assetType Asset Type to transfer + /// @param rest How much of the amount left after previous transfers + /// @param amount Total amount of the Asset. Used as a base to calculate part from (100%) + /// @param fees Array of LibPart.Part which represents fees to pay + /// @param from owner of the Asset to transfer + /// @return newRest how much left after transferring fees + /// @return totalFees total number of fees in bp + function transferFees( + LibAsset.AssetType memory assetType, + uint256 rest, + uint256 amount, + LibPart.Part[] memory fees, + address from + ) internal returns (uint256 newRest, uint256 totalFees) { + totalFees = 0; + newRest = rest; + for (uint256 i = 0; i < fees.length; ++i) { + totalFees = totalFees + fees[i].value; + uint256 feeValue; + (newRest, feeValue) = subFeeInBp(newRest, amount, fees[i].value); + if (feeValue > 0) { + transfer(LibAsset.Asset(assetType, feeValue), from, fees[i].account); + } + } + } + + /// @notice transfers main part of the asset (payout) + /// @param assetType Asset Type to transfer + /// @param amount Amount of the asset to transfer + /// @param from Current owner of the asset + /// @param payouts List of payouts - receivers of the Asset + function transferPayouts( + LibAsset.AssetType memory assetType, + uint256 amount, + address from, + LibPart.Part[] memory payouts + ) internal { + require(payouts.length > 0, "transferPayouts: nothing to transfer"); + uint256 sumBps = 0; + uint256 rest = amount; + for (uint256 i = 0; i < payouts.length - 1; ++i) { + uint256 currentAmount = amount.bp(payouts[i].value); + sumBps = sumBps + payouts[i].value; + if (currentAmount > 0) { + rest = rest - currentAmount; + transfer(LibAsset.Asset(assetType, currentAmount), from, payouts[i].account); + } + } + LibPart.Part memory lastPayout = payouts[payouts.length - 1]; + sumBps = sumBps + lastPayout.value; + require(sumBps == 10000, "Sum payouts Bps not equal 100%"); + if (rest > 0) { + transfer(LibAsset.Asset(assetType, rest), from, lastPayout.account); + } + } + + /// @notice calculates total amount of fee-side asset that is going to be used in match + /// @param amount fee-side order value + /// @param orderOriginFees fee-side order's origin fee (it adds on top of the amount) + /// @param maxFeesBasePoint max fee for the sell-order (used and is > 0 for V3 orders only) + /// @return total amount of fee-side asset + function calculateTotalAmount( + uint256 amount, + LibPart.Part[] memory orderOriginFees, + uint256 maxFeesBasePoint + ) internal pure returns (uint256) { + if (maxFeesBasePoint > 0) { + return amount; + } + uint256 fees = 0; + for (uint256 i = 0; i < orderOriginFees.length; ++i) { + require(orderOriginFees[i].value <= 10000, "origin fee is too big"); + fees = fees + orderOriginFees[i].value; + } + return amount + amount.bp(fees); + } + + /// @notice subtract fees in BP, or base point + /// @param value amount left from amount after fees are discounted + /// @param total total price for asset + /// @param feeInBp fee in basepoint to be deducted + function subFeeInBp( + uint256 value, + uint256 total, + uint256 feeInBp + ) internal pure returns (uint256 newValue, uint256 realFee) { + return subFee(value, total.bp(feeInBp)); + } + + /// @notice subtract fee from value + /// @param value from which the fees will be deducted + /// @param fee to deduct from value + /// @return newValue result from deduction, 0 if value < fee + /// @return realFee fee value if value > fee, otherwise return value input + function subFee(uint256 value, uint256 fee) internal pure returns (uint256 newValue, uint256 realFee) { + if (value > fee) { + newValue = value - fee; + realFee = fee; + } else { + newValue = 0; + realFee = value; + } + } + + uint256[46] private __gap; +} diff --git a/packages/marketplace/src/transfer-manager/interfaces/IRoyaltyUGC.sol b/packages/marketplace/src/transfer-manager/interfaces/IRoyaltyUGC.sol new file mode 100644 index 0000000000..2f7b89cfda --- /dev/null +++ b/packages/marketplace/src/transfer-manager/interfaces/IRoyaltyUGC.sol @@ -0,0 +1,6 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IRoyaltyUGC { + function getCreatorAddress(uint256 tokenId) external pure returns (address creator); +} diff --git a/packages/marketplace/src/transfer-manager/interfaces/ITransferExecutor.sol b/packages/marketplace/src/transfer-manager/interfaces/ITransferExecutor.sol new file mode 100644 index 0000000000..240422d02b --- /dev/null +++ b/packages/marketplace/src/transfer-manager/interfaces/ITransferExecutor.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibAsset} from "../../lib-asset/LibAsset.sol"; + +abstract contract ITransferExecutor { + function transfer(LibAsset.Asset memory asset, address from, address to) internal virtual; +} diff --git a/packages/marketplace/src/transfer-manager/interfaces/ITransferManager.sol b/packages/marketplace/src/transfer-manager/interfaces/ITransferManager.sol new file mode 100644 index 0000000000..25c639085e --- /dev/null +++ b/packages/marketplace/src/transfer-manager/interfaces/ITransferManager.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibDeal} from "../lib/LibDeal.sol"; +import {ITransferExecutor} from "./ITransferExecutor.sol"; + +abstract contract ITransferManager is ITransferExecutor { + function doTransfers( + LibDeal.DealSide memory left, + LibDeal.DealSide memory right, + LibDeal.DealData memory dealData + ) internal virtual returns (uint256 totalMakeValue, uint256 totalTakeValue); +} diff --git a/packages/marketplace/src/transfer-manager/lib/LibDeal.sol b/packages/marketplace/src/transfer-manager/lib/LibDeal.sol new file mode 100644 index 0000000000..1349d44a41 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/lib/LibDeal.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibPart} from "../../lib-part/LibPart.sol"; +import {LibAsset} from "../../lib-asset/LibAsset.sol"; +import {LibFeeSide} from "./LibFeeSide.sol"; + +library LibDeal { + struct DealSide { + LibAsset.Asset asset; + LibPart.Part[] payouts; + LibPart.Part[] originFees; + address from; + } + + struct DealData { + uint256 protocolFee; // TODO: check if necessary + uint256 maxFeesBasePoint; + LibFeeSide.FeeSide feeSide; + } +} diff --git a/packages/marketplace/src/transfer-manager/lib/LibFeeSide.md b/packages/marketplace/src/transfer-manager/lib/LibFeeSide.md new file mode 100644 index 0000000000..5def6da084 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/lib/LibFeeSide.md @@ -0,0 +1,3 @@ +#### Features + +`getFeeSide` function identifies side of the order which can be interpreted like money. So [RaribleTransferManager](./RaribleTransferManager.sol) is able to calculate and transfer different fees (royalties, protocol and origin fees etc). \ No newline at end of file diff --git a/packages/marketplace/src/transfer-manager/lib/LibFeeSide.sol b/packages/marketplace/src/transfer-manager/lib/LibFeeSide.sol new file mode 100644 index 0000000000..8aabea0fce --- /dev/null +++ b/packages/marketplace/src/transfer-manager/lib/LibFeeSide.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibAsset} from "../../lib-asset/LibAsset.sol"; + +library LibFeeSide { + enum FeeSide { + NONE, + LEFT, + RIGHT + } + + function getFeeSide(bytes4 leftClass, bytes4 rightClass) internal pure returns (FeeSide) { + if (leftClass == LibAsset.ETH_ASSET_CLASS) { + return FeeSide.LEFT; + } + if (rightClass == LibAsset.ETH_ASSET_CLASS) { + return FeeSide.RIGHT; + } + if (leftClass == LibAsset.ERC20_ASSET_CLASS) { + return FeeSide.LEFT; + } + if (rightClass == LibAsset.ERC20_ASSET_CLASS) { + return FeeSide.RIGHT; + } + if (leftClass == LibAsset.ERC1155_ASSET_CLASS) { + return FeeSide.LEFT; + } + if (rightClass == LibAsset.ERC1155_ASSET_CLASS) { + return FeeSide.RIGHT; + } + return FeeSide.NONE; + } +} diff --git a/packages/marketplace/src/transfer-manager/lib/LibTransfer.sol b/packages/marketplace/src/transfer-manager/lib/LibTransfer.sol new file mode 100644 index 0000000000..844a6a750f --- /dev/null +++ b/packages/marketplace/src/transfer-manager/lib/LibTransfer.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +library LibTransfer { + function transferEth(address payable to, uint256 value) internal { + (bool success, ) = to.call{value: value}(""); + require(success, "transfer failed"); + } +} diff --git a/packages/marketplace/src/transfer-manager/mocks/LibFeeSideTest.sol b/packages/marketplace/src/transfer-manager/mocks/LibFeeSideTest.sol new file mode 100644 index 0000000000..92f719d21a --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/LibFeeSideTest.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {LibFeeSide} from "../lib/LibFeeSide.sol"; + +contract LibFeeSideTest { + function getFeeSideTest(bytes4 maker, bytes4 taker) external pure returns (LibFeeSide.FeeSide) { + return LibFeeSide.getFeeSide(maker, taker); + } +} diff --git a/packages/marketplace/src/transfer-manager/mocks/SimpleTest.sol b/packages/marketplace/src/transfer-manager/mocks/SimpleTest.sol new file mode 100644 index 0000000000..ebcfe52502 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/SimpleTest.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {TransferManager} from "../TransferManager.sol"; +import {LibERC721LazyMint} from "../../lazy-mint/erc-721/LibERC721LazyMint.sol"; +import {LibERC1155LazyMint, LibPart} from "../../lazy-mint/erc-1155/LibERC1155LazyMint.sol"; +import {TransferExecutor, LibAsset} from "../TransferExecutor.sol"; + +contract SimpleTest is TransferManager, TransferExecutor { + function getRoyaltiesByAssetTest(LibAsset.AssetType memory matchNft) external returns (LibPart.Part[] memory) { + return getRoyaltiesByAssetType(matchNft); + } + + function encode721(LibERC721LazyMint.Mint721Data memory data) external view returns (bytes memory) { + return abi.encode(address(this), data); + } + + function encode1155(LibERC1155LazyMint.Mint1155Data memory data) external view returns (bytes memory) { + return abi.encode(address(this), data); + } +} diff --git a/packages/marketplace/src/transfer-manager/mocks/TestERC1155.sol b/packages/marketplace/src/transfer-manager/mocks/TestERC1155.sol new file mode 100644 index 0000000000..220cd2b082 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/TestERC1155.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC1155} from "../../test/TestERC1155.sol"; diff --git a/packages/marketplace/src/transfer-manager/mocks/TestERC20.sol b/packages/marketplace/src/transfer-manager/mocks/TestERC20.sol new file mode 100644 index 0000000000..a9c230f8cb --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/TestERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC20} from "../../test/TestERC20.sol"; diff --git a/packages/marketplace/src/transfer-manager/mocks/TestERC20ZRX.sol b/packages/marketplace/src/transfer-manager/mocks/TestERC20ZRX.sol new file mode 100644 index 0000000000..47f50b0e3a --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/TestERC20ZRX.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC20ZRX} from "../../test/TestERC20ZRX.sol"; diff --git a/packages/marketplace/src/transfer-manager/mocks/TestERC721.sol b/packages/marketplace/src/transfer-manager/mocks/TestERC721.sol new file mode 100644 index 0000000000..b310c94706 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/TestERC721.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +// solhint-disable-next-line no-unused-import +import {TestERC721} from "../../test/TestERC721.sol"; diff --git a/packages/marketplace/src/transfer-manager/mocks/TransferExecutorTest.sol b/packages/marketplace/src/transfer-manager/mocks/TransferExecutorTest.sol new file mode 100644 index 0000000000..addb0ab6e2 --- /dev/null +++ b/packages/marketplace/src/transfer-manager/mocks/TransferExecutorTest.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {TransferExecutor, Initializable, OwnableUpgradeable, LibAsset} from "../TransferExecutor.sol"; + +contract TransferExecutorTest is Initializable, OwnableUpgradeable, TransferExecutor { + function __TransferExecutorTest_init() external initializer { + __Ownable_init(); + } + + function transferTest(LibAsset.Asset calldata asset, address from, address to) external payable { + TransferExecutor.transfer(asset, from, to); + } +}