|
// SPDX-License-Identifier: GPL-3.0 |
|
|
|
pragma solidity >=0.7.0 <0.9.0; |
|
|
|
// import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; |
|
|
|
interface IERC721 { |
|
function ownerOf(uint256 tokenId) external view returns (address owner); |
|
function transferFrom(address from, address to, uint256 tokenId) external; |
|
} |
|
|
|
// in case someone try to send NFTs |
|
interface IERC721Receiver { |
|
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); |
|
} |
|
|
|
// for the deployer withdraw when someone wrongly send ERC20 tokens here. |
|
interface IERC20 { |
|
function transfer(address to, uint256 amount) external returns (bool); |
|
} |
|
|
|
contract BasicMarketplace is IERC721Receiver { |
|
struct Order { |
|
address creator; |
|
uint tokenId; |
|
uint price; |
|
uint dateCreated; |
|
uint8 status; // 0 = never had offers, 1 = pending/opened, 2 = fulfilled |
|
} |
|
|
|
// owner/creator, tokenId, price, dateCreated, status |
|
event OrderCreated(address, uint, uint, uint, uint); |
|
|
|
// from, to, tokenId, price, dateCreated, status |
|
event OrderFulfilled(address, address, uint, uint, uint, uint); |
|
|
|
event OrderUpdated(address, uint, uint, uint, uint); |
|
event OrderCanceled(address, uint, uint, uint, uint); |
|
|
|
|
|
IERC721 public nftContract; |
|
address public deployer; |
|
mapping(uint => Order) public orders; |
|
|
|
constructor(address _nftContract) { |
|
nftContract = IERC721(_nftContract); |
|
deployer = msg.sender; |
|
} |
|
|
|
modifier onlyOwnerOfToken(uint tokenId) { |
|
require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "BasicMarketplace: not owner of token"); |
|
_; |
|
} |
|
|
|
modifier onlyOpened(uint tokenId) { |
|
Order memory order = orders[tokenId]; |
|
require(order.status == 1, "BasicMarketplace: there is no order for this token or order is closed"); |
|
// delete order; // should we?? |
|
_; |
|
} |
|
|
|
modifier onlyEOA() { |
|
// only allowing externally-owned addresses (users, not contracts). |
|
require(msg.sender == tx.origin, "BasicMarketplace: Must use EOA, only users can call this method"); |
|
_; |
|
} |
|
|
|
receive() external payable { |
|
revert("BasicMarketplace: do not send anything here, or contact the deployer, it's the only one that can withdraw stuck tokens"); |
|
} |
|
|
|
function withdraw(address tokenAddress, address toAddress, uint amount) public { |
|
require(msg.sender == deployer, "BasicMarketplace: only deployer can withdraw stuck tokens."); |
|
IERC20(tokenAddress).transfer(toAddress, amount); |
|
} |
|
|
|
function onERC721Received( |
|
address, |
|
address, |
|
uint256, |
|
bytes memory |
|
) public virtual override returns (bytes4) { |
|
require(tx.origin == address(0), "BasicMarketplace: not allowed to send NFT tokens here"); |
|
|
|
return bytes4(abi.encodePacked("intentionally invalid")); |
|
} |
|
|
|
function updateNFTContract(address _addr) public onlyEOA { |
|
nftContract = IERC721(_addr); |
|
} |
|
|
|
function getOrder(uint tokenId) public view returns (Order memory) { |
|
return orders[tokenId]; |
|
} |
|
|
|
function createOrder(uint tokenId, uint price) public onlyOwnerOfToken(tokenId) onlyEOA { |
|
Order memory order = orders[tokenId]; |
|
|
|
if (order.status == 1) { |
|
revert("BasicMarketplace: there is opened order for this token id"); |
|
} |
|
|
|
uint dateCreated = block.timestamp; |
|
orders[tokenId] = Order(msg.sender, tokenId, price, dateCreated, 1); |
|
|
|
// delete order; // should we?? |
|
emit OrderCreated(msg.sender, tokenId, price, dateCreated, 1); |
|
} |
|
|
|
function updateOrder(uint tokenId, uint price) public onlyOwnerOfToken(tokenId) onlyOpened(tokenId) onlyEOA { |
|
Order memory order = orders[tokenId]; |
|
|
|
orders[tokenId] = Order(msg.sender, tokenId, price, order.dateCreated, 1); |
|
// delete order; // should we?? |
|
emit OrderUpdated(msg.sender, tokenId, order.price, order.dateCreated, 1); |
|
} |
|
|
|
function cancelOrder(uint tokenId) public onlyOwnerOfToken(tokenId) onlyOpened(tokenId) onlyEOA { |
|
Order memory order = orders[tokenId]; |
|
|
|
// `delete` refunds ~15k, it's cheaper |
|
delete orders[tokenId]; |
|
// orders[tokenId] = Order(address(0), 0, 0, 0, 0); |
|
|
|
// delete order; // should we?? |
|
emit OrderCanceled(msg.sender, tokenId, order.price, order.dateCreated, 0); |
|
} |
|
|
|
function fulfillOrder(uint tokenId) external payable onlyOpened(tokenId) onlyEOA { |
|
Order memory order = orders[tokenId]; |
|
|
|
require(order.status == 1, "BasicMarketplace: no open order for this token, create an order first"); |
|
// higher than date created plus 5 minutes? bots protection or whatever? |
|
require(block.timestamp > order.dateCreated, "BasicMarketplace: order is not yet ready to fulfill"); |
|
require(msg.sender != order.creator, "BasicMarketplace: order creator cannot sell to self, better to cancel"); |
|
require(msg.value == order.price, "BasicMarketplace: invalid ask price amount"); |
|
|
|
// reset state, reentrancy protect |
|
// orders[tokenId] = Order(address(0), 0, 0, 0, 0); |
|
|
|
// `delete` refunds ~15k, it's cheaper |
|
delete orders[tokenId]; |
|
// delete order; // should we?? |
|
|
|
// transfer the nft to the buyer |
|
IERC721(nftContract).transferFrom(order.creator, msg.sender, tokenId); |
|
|
|
// transfer ETH from the sender/contract to the seller |
|
payable(order.creator).transfer(order.price); |
|
|
|
emit OrderFulfilled(order.creator, msg.sender, tokenId, msg.value, block.timestamp, 0); |
|
} |
|
} |