Skip to content

Instantly share code, notes, and snippets.

@tunnckoCore
Last active August 27, 2022 14:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tunnckoCore/537f63ecb73e3dd542e7eef7325f90ec to your computer and use it in GitHub Desktop.
Save tunnckoCore/537f63ecb73e3dd542e7eef7325f90ec to your computer and use it in GitHub Desktop.
Secure, cheap, and fast independent Solidity NFT Marketplace, in 100 lines. Not relying on external APIs, indexers, or oracles.

Basic Solidity NFT Marketplace, in ~150 lines

  • Marketplace contract Rinkeby - 0x734225699881BD4ba3611Bd29B4D396c5Bc2C523
    • hint: use updateNFTContract to add your NFT project
  • Dummy ERC721 NFT contract Rinkeby - 0xe1Bc4E2Dc13aa497211e367c1FB9D209d35baBbc, everyone can mint
  • ERC721 NFTs only, pay in ETH only
  • holds no funds, no tokens, no NFTs; mistakes resistent (refunds if you send)
    • if ERC20 tokens are send, only the deployer of the Marketplace contract can withdraw them to given address
  • only EOA (users) can use it (call methods), no bots or contracts
  • perpetual listings, can update the price only or cancel the listing
  • no royalties/fees (by default, but it's easy to add)
  • no dependencies, only needs ERC721.ownerOf and ERC721.transferFrom
  • no ERC721/nft tokens are transferred to the contract, this happens only on buy
  • no "offers" functionality, scam/mistake prone
  • operations: createOrder, updateOrder, cancelOrder, and fulfillOrder (e.g. buy)
  • only owner of the NFT token can create, update or cancel order
  • only non-owner can fulfill order (buy)
  • reentrancy & bot protections
  • emitting events
  • useful for "exclusive" marketplaces
    • (e.g. specifically build only for a certain collection)

To work properly, after deploying it with your NFT collection contract (which you want the marketplace to be for) passed in the constructor or by using updateNFTContract, you should "approve" the Marketplace contract in your NFT contract. That's could usually be done by the App or by the user manually. Normal stuff. It cannot be handled automatically, because you cannot use ERC721's approve or setApprovalForAll set to the marketplace contract, because when called from the marketplace contract the NFT contract will see msg.sender not as the user (which calls certain function), but as the marketplace contract.

// 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);
}
}
// 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");
_;
}
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);
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);
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];
orders[tokenId] = Order(address(0), 0, 0, 0, 2);
emit OrderCanceled(msg.sender, tokenId, order.price, order.dateCreated, 2);
}
function fulfillOrder(uint tokenId) external payable onlyOpened(tokenId) onlyEOA {
Order memory order = orders[tokenId];
// 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, 2);
// 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, order.price, block.timestamp, 2);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment