WTF Solidity 38. NFT Exchange
I have been revisiting Solidity lately to review the details and create a "WTF Solidity Tutorial" for beginners (professional programmers may find other tutorials more suitable), with 1-3 updates per week.
Twitter: @0xAA_Science
Discord: WTF Academy
All code and tutorials are open source on Github: github.com/AmazingAng/WTFSolidity
"Opensea" is the largest NFT trading platform on Ethereum with a total trading volume of $30 billion. Opensea charges a fee of 2.5% on transactions, meaning it has made at least $750 million in profits through user transactions. Additionally, its operation is not decentralized, and it has no plans to issue coins to compensate users. NFT players have been frustrated with Opensea for a long time. Today, we use smart contracts to build a zero-fee decentralized NFT exchange: NFTSwap.
Design Logic
- Seller: The party selling the NFT can list the item, revoke the listing, and update the price.
- Buyer: The party buying the NFT can purchase the item.
- Order: The on-chain NFT order published by the seller. A series of the same tokenId can have a maximum of one order, which includes the listing price and owner information. When an order is completed or revoked, the information is cleared.
NFTSwap Contract
Events
The contract includes four events corresponding to the actions of listing (list), revoking (revoke), updating the price (update), and purchasing (purchase) the NFT.
event List(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 price);
event Purchase(address indexed buyer, address indexed nftAddr, uint256 indexed tokenId, uint256 price);
event Revoke(address indexed seller, address indexed nftAddr, uint256 indexed tokenId);
event Update(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 newPrice);
Order
An NFT
order is abstracted as the Order
structure, which contains information about the listing price (price
) and the owner (owner
). The nftList
mapping records the NFT
series (contract address) and tokenId
information that the order corresponds to.
// Define the order structure
struct Order{
address owner;
uint256 price;
}
// NFT Order mapping
mapping(address => mapping(uint256 => Order)) public nftList;
Fallback Function
In NFTSwap
, users purchase NFT
using ETH
. Therefore, the contract needs to implement the fallback()
function to receive ETH
.
fallback() external payable{}
onERC721Received
The safe transfer function of ERC721
checks whether the receiving contract has implemented the onERC721Received()
function and returns the correct selector. After the user places an order, the NFT
needs to be sent to the NFTSwap
contract. Therefore, the NFTSwap
contract inherits the IERC721Receiver
interface and implements the onERC721Received()
function.
This is a smart contract named "NFTSwap" that implements the interface "IERC721Receiver". The function "onERC721Received" is defined to receive ERC721 tokens. It takes four parameters:
- "operator": the address that called the function
- "from": the address that transferred the token to the contract
- "tokenId": the ID of the ERC721 token that was transferred
- "data": additional data that can be sent with the token transfer
The function returns the selector of the "onERC721Received" function from "IERC721Receiver" interface.
Trading
The contract implements 4
functions related to trading:
- Listing
list()
: The seller creates anNFT
, creates an order, and releases theList
event. The parameters are theNFT
contract address_nftAddr
, corresponding_tokenId
ofNFT
, and listing price_price
(Note: the unit iswei
). After successful, theNFT
will transfer from the seller to theNFTSwap
contract.
// List: The seller lists NFT on sale, contract address is _nftAddr, tokenId is _tokenId, price is _price in ether (unit is wei)
function list(address _nftAddr, uint256 _tokenId, uint256 _price) public{
IERC721 _nft = IERC721(_nftAddr); // Declare an interface contract variable IERC721
require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // The contract is approved
require(_price > 0); // The price is greater than 0
Order storage _order = nftList[_nftAddr][_tokenId]; // Set the NFT holder and price
_order.owner = msg.sender;
_order.price = _price;
// Transfer NFT to the contract
_nft.safeTransferFrom(msg.sender, address(this), _tokenId);
// Release List event
emit List(msg.sender, _nftAddr, _tokenId, _price);
}
revoke()
: Seller cancels the order and releases theRevoke
event. Parameters include theNFT
contract address_nftAddr
and the corresponding_tokenId
. After successful execution, theNFT
will be returned to the seller from theNFTSwap
contract.
// cancel order: seller cancels the order
function revoke(address _nftAddr, uint256 _tokenId) public {
Order storage _order = nftList[_nftAddr][_tokenId]; // get the order
require(_order.owner == msg.sender, "Not Owner"); // must be initiated by the owner
// declare IERC721 interface contract variables
IERC721 _nft = IERC721(_nftAddr);
require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract
// transfer NFT to seller
_nft.safeTransferFrom(address(this), msg.sender, _tokenId);
delete nftList[_nftAddr][_tokenId]; // delete order
// emit Revoke event
emit Revoke(msg.sender, _nftAddr, _tokenId);
}
- Modify price
update()
: The seller modifies the price of the NFT order and releases theUpdate
event. The parameters are the NFT contract address_nftAddr
, the corresponding_tokenId
of the NFT, and the updated order price_newPrice
(Note: The unit iswei
).
// Adjust Price: Seller adjusts the listing price
function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public {
require(_newPrice > 0, "Invalid Price"); // NFT price must be greater than 0
Order storage _order = nftList[_nftAddr][_tokenId]; // Get the Order
require(_order.owner == msg.sender, "Not Owner"); // It must be initiated by the owner
// Declare IERC721 interface contract variable
IERC721 _nft = IERC721(_nftAddr);
require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract
// Adjust the NFT price
_order.price = _newPrice;
// Release Update event
emit Update(msg.sender, _nftAddr, _tokenId, _newPrice);
}
- Purchase: The buyer pays with
ETH
to purchase theNFT
on the order, and triggers thePurchase
event. The parameters are theNFT
contract address_nftAddr
and the corresponding_tokenId
of theNFT
. Upon success, theETH
will be transferred to the seller and theNFT
will be transferred from theNFTSwap
contract to the buyer.
// Purchase: A buyer purchases an NFT with ETH attached, the contract address is _nftAddr, tokenId is _tokenId
function purchase(address _nftAddr, uint256 _tokenId) payable public {
Order storage _order = nftList[_nftAddr][_tokenId]; // Get Order
require(_order.price > 0, "Invalid Price"); // The NFT price is greater than 0
require(msg.value >= _order.price, "Increase price"); // The purchase price is greater than the asking price
// Declare IERC721 interface contract variable
IERC721 _nft = IERC721(_nftAddr);
require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // The NFT is in the contract
// Transfer the NFT to the buyer
_nft.safeTransferFrom(address(this), msg.sender, _tokenId);
// Transfer ETH to the seller, refund any excess ETH to the buyer
payable(_order.owner).transfer(_order.price);
payable(msg.sender).transfer(msg.value-_order.price);
delete nftList[_nftAddr][_tokenId]; // Delete order
// Release Purchase event
emit Purchase(msg.sender, _nftAddr, _tokenId, msg.value);
}
Implementation in Remix
1. Deploy the NFT contract
Refer to the ERC721 tutorial to learn about NFTs and deploy the WTFApe
NFT contract.
Mint the first NFT to yourself. This is done so that you can perform operations such as listing the NFT and modifying its price in the future.
The mint(address to, uint tokenId)
function takes two parameters:
to
: The address to which the NFT will be minted. This is usually your own wallet address.
tokenId
: Since the WTFApe
contract defines a total of 10,000 NFTs, the first two NFTs to be minted here have tokenId
values of 0
and 1
, respectively.
In the WTFApe
contract, use ownerOf
to confirm that you own the NFT with tokenId
equal to 0.
The ownerOf(uint tokenId)
function takes one parameter:
tokenId
: tokenId
is the unique identifier of the NFT, and in this example, it refers to the 0
id generated during the minting process described above.
Using the above method, mint NFTs with tokenId
0
and 1
for yourself. For tokenId
0
, execute a purchase update operation, and for tokenId
1
, execute a delisting operation.
2. Deploying the NFTSwap
contract
Deploy the NFTSwap
contract.
3. Authorizing the NFTSwap
contract to list the NFT for sale
In the WTFApe
contract, call the approve()
authorization function to grant permission for the NFTSwap
contract to list the tokenId
0
NFT that you own for sale.
The approve(address to, uint tokenId)
method has 2 parameters:
to
: The address tokenId
will be authorized to be transferred to, in this case, the address of the NFTSwap
contract.
tokenId
: tokenId
is the unique identifier of the NFT, and in this example, it refers to the 0
id generated during the minting process described above.
Following the method above, authorizes the NFT with tokenId
of 1
to the NFTSwap
contract address.
4. List the NFT for Sale
Call the list()
function of the NFTSwap
contract to list the NFT with tokenId
of 0
that is held by the caller on the NFTSwap
. Set the price to 1 wei
.
The list(address _nftAddr, uint256 _tokenId, uint256 _price)
method has 3 parameters:
_nftAddr
: _nftAddr
is the NFT contract address, which in this case is the WTFApe
contract address.
_tokenId
: _tokenId
is the ID of the NFT, which in this case is the minted 0
ID mentioned above.
_price
: _price
is the price of the NFT, which in this case is 1 wei
.
Following the above method, list the NFT with tokenId
of 1
that is held by the caller on the NFTSwap
and set the price to 1 wei
.
5. View Listed NFTs.
Call the nftList()
function of the NFTSwap
contract to view the listed NFT.
nftList
: is a mapping of NFT Orders with the following structure:
nftList[_nftAddr][_tokenId]
: Input _nftAddr
and _tokenId
, and return an NFT order.
6. Update NFT Price
Call the update()
function of the NFTSwap
contract to update the price of NFT with tokenId
0 to 77 wei
.
The update(address _nftAddr, uint256 _tokenId, uint256 _newPrice)
method has three parameters:
_nftAddr
: _nftAddr
is the address of the NFT contract, which in this case is the WTFApe
contract address.
_tokenId
: _tokenId
is the id of the NFT, which in this case is 0, the id of the minted NFT mentioned above.
_newPrice
: _newPrice
is the new price of the NFT, which in this case is 77 wei
.
After executing update()
, call nftList
to view the updated price.
5. Dismantle NFT
Call the revoke()
function of the NFTSwap
contract to dismantle the NFT.
In the above article, we put up two NFTs with tokenId
of 0
and 1
, respectively. In this method, we are dismantling the NFT with tokenId
as 1
.
The revoke(address _nftAddr, uint256 _tokenId)
function has 2 parameters:
_nftAddr
: The _nftAddr
is the address of the NFT contract, which is the WTFApe
contract address in this example.
_tokenId
: The _tokenId
is the id of the NFT, which is the 1
Id for the minting in this example.
Call the nftList()
function of the NFTSwap
contract to see that the NFT has been dismantled. It will require reauthorization to put it up again.
Note that after taking down the NFT, you need to start again from step 3, authorize and relist the NFT before purchasing.
6. Purchase NFT
Switch to another account and call the purchase()
function of the NFTSwap
contract to buy an NFT. When purchasing, you need to input the NFT
contract address, tokenId
, and the amount of ETH
you want to pay.
We took down the NFT with tokenId
1, but there is still an NFT with tokenId
0 available for purchase.
The purchase(address _nftAddr, uint256 _tokenId, uint256 _wei)
method has three parameters:
_nftAddr
: _nftAddr
is the NFT contract address, which is the WTFApe
contract address in this example.
_tokenId
: _tokenId
is the ID of the NFT, which is 0 as we minted it earlier.
_wei
: _wei
is the amount of ETH
to be paid, which is 77 wei
in this example.
7. Verify change of NFT owner.
After a successful purchase, calling the ownerOf()
function of the WTFApe
contract shows that the NFT
owner has changed, indicating a successful purchase!
In summary, in this lecture, we built a zero-fee decentralized NFT
exchange. Although OpenSea
has made significant contributions to the development of NFTs
, its disadvantages are also very obvious: high transaction fees, no reward for users, and trading mechanisms that can easily lead to phishing attacks, causing users to lose their assets. Currently, new NFT
trading platforms such as Looksrare
and dydx
are challenging the position of OpenSea
, and Uniswap
is also researching new NFT
exchanges. We believe that in the near future, we will have better NFT
exchanges to use.