git.delta.rocks / unique-network / refs/commits / a478235c0b6e

difftreelog

source

js-packages/tests/eth/marketplace-v2/Market.sol12.5 KiBsourcehistory
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.18;34import "@openzeppelin/contracts/security/ReentrancyGuard.sol";5import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";6import "@openzeppelin/contracts/token/ERC721/IERC721.sol";7import "@openzeppelin/contracts/access/Ownable.sol";8import { UniqueNFT, CrossAddress } from "@unique-nft/solidity-interfaces/contracts/UniqueNFT.sol";9import { UniqueFungible, CrossAddress as CrossAddressF } from "@unique-nft/solidity-interfaces/contracts/UniqueFungible.sol";10import { CollectionHelpers } from "@unique-nft/solidity-interfaces/contracts/CollectionHelpers.sol";11import "./royalty/UniqueRoyaltyHelper.sol";1213contract Market is Ownable, ReentrancyGuard {14    using ERC165Checker for address;1516    struct Order {17      uint32 id;18      uint32 collectionId;19      uint32 tokenId;20      uint32 amount;21      uint256 price;22      CrossAddress seller;23    }2425    uint32 public constant version = 0;26    uint32 public constant buildVersion = 3;27    bytes4 private constant InterfaceId_ERC721 = 0x80ac58cd;28    bytes4 private constant InterfaceId_ERC165 = 0x5755c3f2;29    CollectionHelpers private constant collectionHelpers =30        CollectionHelpers(0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F);3132    mapping(uint32 => mapping(uint32 => Order)) orders;33    uint32 private idCount = 1;34    uint32 public marketFee;35    uint64 public ctime;36    address public ownerAddress;37    mapping(address => bool) public admins;3839    event TokenIsUpForSale(uint32 version, Order item);40    event TokenRevoke(uint32 version, Order item, uint32 amount);41    event TokenIsApproved(uint32 version, Order item);42    event TokenIsPurchased(43      uint32 version,44      Order item,45      uint32 salesAmount,46      CrossAddress buyer,47      RoyaltyAmount[] royalties48    );49    event Log(string message);5051    error InvalidArgument(string info);52    error InvalidMarketFee();53    error SellerIsNotOwner();54    error TokenIsAlreadyOnSale();55    error TokenIsNotApproved();56    error CollectionNotFound();57    error CollectionNotSupportedERC721();58    error OrderNotFound();59    error TooManyAmountRequested();60    error NotEnoughMoneyError();61    error InvalidRoyaltiesError(uint256 totalRoyalty);62    error FailTransferToken(string reason);6364    modifier onlyAdmin() {65      require(msg.sender == this.owner() || admins[msg.sender], "Only admin can");66      _;67    }6869    modifier validCrossAddress(address eth, uint256 sub) {70      if (eth == address(0) && sub == 0) {71        revert InvalidArgument("Ethereum and Substrate addresses cannot be null at the same time");72      }7374      if (eth != address(0) && sub != 0) {75        revert InvalidArgument("Ethereum and Substrate addresses cannot be not null at the same time");76      }7778      _;79    }8081    constructor(uint32 fee, uint64 timestamp) {82        marketFee = fee;83        ctime = timestamp;8485        if (marketFee >= 100) {86            revert InvalidMarketFee();87        }88    }8990    /**91     * Fallback that allows this contract to receive native token.92     * We need this for self-sponsoring93     */94    fallback() external payable {}9596    /**97     * Receive also allows this contract to receive native token.98     * We need this for self-sponsoring99     */100    receive() external payable {}101102    function getErc721(uint32 collectionId) private view returns (IERC721) {103        address collectionAddress = collectionHelpers.collectionAddress(104            collectionId105        );106107        uint size;108        assembly {109            size := extcodesize(collectionAddress)110        }111112        if (size == 0) {113            revert CollectionNotFound();114        }115116        if (!collectionAddress.supportsInterface(InterfaceId_ERC721)) {117            revert CollectionNotSupportedERC721();118        }119120        return IERC721(collectionAddress);121    }122123    /**124     * Add new admin. Only owner or an existing admin can add admins.125     *126     * @param admin: Address of a new admin to add127     */128    function addAdmin(address admin) public onlyAdmin {129      admins[admin] = true;130    }131132    /**133     * Remove an admin. Only owner or an existing admin can remove admins.134     *135     * @param admin: Address of a new admin to add136     */137    function removeAdmin(address admin) public onlyAdmin {138      delete admins[admin];139    }140141    /**142     * Place an NFT or RFT token for sale. It must be pre-approved for transfers by this contract address.143     *144     * @param collectionId: ID of the token collection145     * @param tokenId: ID of the token146     * @param price: Price (with proper network currency decimals)147     * @param amount: Number of token fractions to list (must always be 1 for NFT)148     * @param seller: The seller cross-address (the beneficiary account to receive payment, may be different from transaction sender)149     */150    function put(151        uint32 collectionId,152        uint32 tokenId,153        uint256 price,154        uint32 amount,155        CrossAddress memory seller156    ) public validCrossAddress(seller.eth, seller.sub) {157        if (price == 0) {158          revert InvalidArgument("price must not be zero");159        }160        if (amount == 0) {161          revert InvalidArgument("amount must not be zero");162        }163164        if (orders[collectionId][tokenId].price > 0) {165            revert TokenIsAlreadyOnSale();166        }167168        IERC721 erc721 = getErc721(collectionId);169170        if (erc721.ownerOf(tokenId) != msg.sender) {171          revert SellerIsNotOwner();172        }173174        if (erc721.getApproved(tokenId) != address(this)) {175          revert TokenIsNotApproved();176        }177178        Order memory order = Order(179            0,180            collectionId,181            tokenId,182            amount,183            price,184            seller185        );186187        order.id = idCount++;188        orders[collectionId][tokenId] = order;189190        emit TokenIsUpForSale(version, order);191    }192193    /**194     * Get information about the listed token order195     *196     * @param collectionId: ID of the token collection197     * @param tokenId: ID of the token198     * @return The order information199     */200    function getOrder(201        uint32 collectionId,202        uint32 tokenId203    ) external view returns (Order memory) {204        return orders[collectionId][tokenId];205    }206207    /**208     * Revoke the token from the sale. Only the original lister can use this method.209     *210     * @param collectionId: ID of the token collection211     * @param tokenId: ID of the token212     * @param amount: Number of token fractions to de-list (must always be 1 for NFT)213     */214    function revoke(215        uint32 collectionId,216        uint32 tokenId,217        uint32 amount218    ) external {219        if (amount == 0) {220          revert InvalidArgument("amount must not be zero");221        }222223        Order memory order = orders[collectionId][tokenId];224225        if (order.price == 0) {226          revert OrderNotFound();227        }228229        if (amount > order.amount) {230          revert TooManyAmountRequested();231        }232233        IERC721 erc721 = getErc721(collectionId);234235        address ethAddress;236        if (order.seller.eth != address(0)) {237          ethAddress = order.seller.eth;238        } else {239          ethAddress = payable(address(uint160(order.seller.sub >> 96)));240        }241        if (erc721.ownerOf(tokenId) != ethAddress) {242          revert SellerIsNotOwner();243        }244245        order.amount -= amount;246        if (order.amount == 0) {247            delete orders[collectionId][tokenId];248        } else {249            orders[collectionId][tokenId] = order;250        }251252        emit TokenRevoke(version, order, amount);253    }254255    /**256     * Test if the token is still approved to be transferred by this contract and delete the order if not.257     *258     * @param collectionId: ID of the token collection259     * @param tokenId: ID of the token260     */261    function checkApproved(uint32 collectionId, uint32 tokenId) public onlyAdmin {262        Order memory order = orders[collectionId][tokenId];263        if (order.price == 0) {264            revert OrderNotFound();265        }266267        IERC721 erc721 = getErc721(collectionId);268269        if (erc721.getApproved(tokenId) != address(this) || erc721.ownerOf(tokenId) != getAddressFromCrossAccount(order.seller)) {270          uint32 amount = order.amount;271          order.amount = 0;272          emit TokenRevoke(version, order, amount);273274          delete orders[collectionId][tokenId];275        } else {276          emit TokenIsApproved(version, order);277        }278    }279280    function getAddressFromCrossAccount(CrossAddress memory account) private pure returns (address) {281        if (account.eth != address(0)) {282            return account.eth;283        } else {284            return address(uint160(account.sub >> 96));285        }286    }287288    /**289     * Revoke the token from the sale. Only the contract admin can use this method.290     *291     * @param collectionId: ID of the token collection292     * @param tokenId: ID of the token293     */294    function revokeAdmin(uint32 collectionId, uint32 tokenId) public onlyAdmin {295        Order memory order = orders[collectionId][tokenId];296        if (order.price == 0) {297          revert OrderNotFound();298        }299300        uint32 amount = order.amount;301        order.amount = 0;302        emit TokenRevoke(version, order, amount);303304        delete orders[collectionId][tokenId];305    }306307    /**308     * Buy a token (partially for an RFT).309     *310     * @param collectionId: ID of the token collection311     * @param tokenId: ID of the token312     * @param amount: Number of token fractions to buy (must always be 1 for NFT)313     * @param buyer: Cross-address of the buyer, eth part must be equal to the transaction signer address314     */315    function buy(316        uint32 collectionId,317        uint32 tokenId,318        uint32 amount,319        CrossAddress memory buyer320    ) public payable validCrossAddress(buyer.eth, buyer.sub) nonReentrant {321        if (msg.value == 0) {322          revert InvalidArgument("msg.value must not be zero");323        }324        if (amount == 0) {325          revert InvalidArgument("amount must not be zero");326        }327328        Order memory order = orders[collectionId][tokenId];329        if (order.price == 0) {330            revert OrderNotFound();331        }332333        if (amount > order.amount) {334            revert TooManyAmountRequested();335        }336337        uint256 totalValue = order.price * amount;338        uint256 feeValue = (totalValue * marketFee) / 100;339340        if (msg.value < totalValue) {341            revert NotEnoughMoneyError();342        }343344        IERC721 erc721 = getErc721(order.collectionId);345        if (erc721.getApproved(tokenId) != address(this)) {346          revert TokenIsNotApproved();347        }348349        order.amount -= amount;350        if (order.amount == 0) {351            delete orders[collectionId][tokenId];352        } else {353            orders[collectionId][tokenId] = order;354        }355356        address collectionAddress = collectionHelpers.collectionAddress(collectionId);357        UniqueNFT nft = UniqueNFT(collectionAddress);358359        nft.transferFromCross(360          order.seller,361          buyer,362          order.tokenId363        );364365        (uint256 totalRoyalty, RoyaltyAmount[] memory royalties) = sendRoyalties(collectionAddress, tokenId, totalValue - feeValue);366367        if (totalRoyalty >= totalValue - feeValue) {368          revert InvalidRoyaltiesError(totalRoyalty);369        }370371        sendMoney(order.seller, totalValue - feeValue - totalRoyalty);372373        if (msg.value > totalValue) {374            sendMoney(buyer, msg.value - totalValue);375        }376377        emit TokenIsPurchased(version, order, amount, buyer, royalties);378    }379380    function sendMoney(CrossAddress memory to, uint256 money) private {381      address collectionAddress = collectionHelpers.collectionAddress(0);382383      UniqueFungible fungible = UniqueFungible(collectionAddress);384385      CrossAddressF memory fromF = CrossAddressF(address(this), 0);386      CrossAddressF memory toF = CrossAddressF(to.eth, to.sub);387388      fungible.transferFromCross(fromF, toF, money);389    }390391    function sendRoyalties(address collection, uint tokenId, uint sellPrice) private returns (uint256, RoyaltyAmount[] memory) {392      RoyaltyAmount[] memory royalties = UniqueRoyaltyHelper.calculate(collection, tokenId, sellPrice);393394      uint256 totalRoyalty = 0;395396      for (uint256 i=0; i<royalties.length; i++) {397        RoyaltyAmount memory royalty = royalties[i];398399        totalRoyalty += royalty.amount;400401        sendMoney(royalty.crossAddress, royalty.amount);402      }403404      return (totalRoyalty, royalties);405    }406407    function withdraw(address transferTo) public onlyOwner {408        uint256 balance = address(this).balance;409410        if (balance > 0) {411            payable(transferTo).transfer(balance);412        }413    }414}