// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "./CryptoHero.sol"; import "./IMarketplace.sol"; /* * Market place to trade heros (should **in theory** be used for any ERC721 token) * It needs an existing hero contract to interact with * Note: it does not inherit from the contract * Note: It takes ownership of the hero for the duration that is is on the marketplace. */ contract MarketPlace is Ownable, IMarketPlace { CryptoHero private _cryptoHero; using SafeMath for uint256; struct Offer { address payable seller; uint256 price; uint256 index; uint256 tokenId; bool active; } bool internal _paused; Offer[] offers; mapping(uint256 => Offer) tokenIdToOffer; mapping(address => uint256) internal _fundsToBeCollected; //Contract has one event that is already declared in the interface. //Contract can be paused by owner to ensure bugs can be fixed after deployment modifier whenNotPaused() { require(!_paused); _; } modifier whenPaused() { require(_paused); _; } function setContract(address _contractAddress) onlyOwner public { _cryptoHero = CryptoHero(_contractAddress); } constructor(address _contractAddress) { setContract(_contractAddress); _paused = false; } function pause() public onlyOwner whenNotPaused { _paused = true; } function resume() public onlyOwner whenPaused { _paused = false; } function isPaused() public view returns (bool) { return _paused; } function getOffer(uint256 _tokenId) public view returns ( address seller, uint256 price, uint256 index, uint256 tokenId, bool active) { require(tokenIdToOffer[_tokenId].active == true, "No active offer at this time"); return (tokenIdToOffer[_tokenId].seller, tokenIdToOffer[_tokenId].price, tokenIdToOffer[_tokenId].index, tokenIdToOffer[_tokenId].tokenId, tokenIdToOffer[_tokenId].active); } function getAllTokensOnSale() public view returns (uint256[] memory listOfOffers) { uint256 resultId = 0;//index for all heros with active offer status (true) for (uint256 index = 0; index < offers.length; index++) { if (offers[index].active == true) { resultId = SafeMath.add(resultId, 1);//determine length of array to return } } if (offers.length == 0) { return new uint256[](0);//returns empty array } else { uint256[] memory allTokensOnSale = new uint256[](resultId); //initialize new array with correct length resultId = 0;//reset index of new array for (uint256 index = 0; index < offers.length; index++) {//iterate through entire offers array if (offers[index].active == true) { allTokensOnSale[resultId] = offers[index].tokenId; resultId = SafeMath.add(resultId, 1); } } return allTokensOnSale; } } function _ownsHero(address _address, uint256 _tokenId) internal view returns (bool) { return (_cryptoHero.ownerOf(_tokenId) == _address); } function setOffer(uint256 _price, uint256 _tokenId) public { require(_ownsHero(msg.sender, _tokenId), "Only the owner of the hero can initialize an offer"); require(tokenIdToOffer[_tokenId].active == false, "You already created an offer for this hero. Please remove it first before creating a new one."); require(_cryptoHero.isApprovedForAll(msg.sender, address(this)), "MarketPlace contract must first be an approved operator for your heros"); Offer memory _currentOffer = Offer({//set offer seller: payable(msg.sender), price: _price, index: offers.length, tokenId: _tokenId, active: true }); tokenIdToOffer[_tokenId] = _currentOffer;//update mapping offers.push(_currentOffer);//update array emit MarketTransaction("Offer created", msg.sender, _tokenId); } function removeOffer(uint256 _tokenId) public { require(tokenIdToOffer[_tokenId].seller == msg.sender, "Only the owner of the hero can withdraw the offer."); offers[tokenIdToOffer[_tokenId].index].active = false; //don't iterate through array, simply set active to false. delete tokenIdToOffer[_tokenId];//delete entry in mapping emit MarketTransaction("Offer removed", msg.sender, _tokenId); } function buyHero(uint256 _tokenId) public payable whenNotPaused{ Offer memory _currentOffer = tokenIdToOffer[_tokenId]; //checks require(_currentOffer.active, "There is no active offer for this hero"); require(msg.value == _currentOffer.price, "The amount offered is not equal to the requested amount"); //effects delete tokenIdToOffer[_tokenId];//delete entry in mapping offers[_currentOffer.index].active = false;//don't iterate through array, but simply set active to false. //interactions if (_currentOffer.price > 0) { _fundsToBeCollected[_currentOffer.seller] = SafeMath.add(_fundsToBeCollected[_currentOffer.seller], _currentOffer.price); //instead of sending money to seller it is deposited in a mapping waiting for seller to pull. } _cryptoHero.transferFrom(_currentOffer.seller, msg.sender, _tokenId);//ERC721 ownership transferred emit MarketTransaction("Hero successfully purchased", msg.sender, _tokenId); } function getBalance() public view returns (uint256) { return _fundsToBeCollected[msg.sender]; } function withdrawFunds() public payable whenNotPaused{ //check require(_fundsToBeCollected[msg.sender] > 0, "No funds available at this time"); uint256 toWithdraw = _fundsToBeCollected[msg.sender]; //effect _fundsToBeCollected[msg.sender] = 0; //interaction payable(msg.sender).transfer(toWithdraw); //making sure transfer executed correctly assert(_fundsToBeCollected[msg.sender] == 0); //emit event emit MonetaryTransaction("Funds successfully received", msg.sender, toWithdraw); } }