# WTF Solidity极简入门: 38. NFT交易所

By [0xAA](https://paragraph.com/@wtfacademy) · 2022-07-14

---

我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。

**推特**：[@WTFAcademy\_](https://twitter.com/WTFAcademy_) ｜[@0xAA\_Science](https://twitter.com/0xAA_Science)

**WTF Academy社群：** [官网 wtf.academy](https://wtf.academy) | [discord](https://discord.wtf.academy) | [微信群申请](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)

所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

* * *

`Opensea`是以太坊上最大的`NFT`交易平台，总交易总量达到了`$300亿`。`Opensea`在交易中抽成`2.5%`，因此它通过用户交易至少获利了`$7.5亿`。另外，它的运作并不去中心化，且不准备发币补偿用户。`NFT`玩家苦`Opensea`久已，今天我们就利用智能合约搭建一个零手续费的去中心化`NFT`交易所：`NFTSwap`。

设计逻辑
----

*   卖家：出售`NFT`的一方，可以挂单`list`、撤单`revoke`、修改价格`update`。
    
*   买家：购买`NFT`的一方，可以购买`purchase`。
    
*   订单：卖家发布的`NFT`链上订。，一个系列的同一`tokenId`最多存在一个订单，其中包含挂单价格`price`和持有人`owner`信息。当一个订单交易完成或被撤单后，其中信息清零。
    

`NFTSwap`合约
-----------

### 事件

合约包含`4`个事件，对应挂单`list`、撤单`revoke`、修改价格`update`、购买`purchase`这四个行为：

        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);
    

### 订单

`NFT`订单抽象为`Order`结构体，包含挂单价格`price`和持有人`owner`信息。`nftList`映射记录了订单是对应的`NFT`系列（合约地址）和`tokenId`信息。

        // 定义order结构体
        struct Order{
            address owner;
            uint256 price; 
        }
        // NFT Order映射
        mapping(address => mapping(uint256 => Order)) public nftList;
    

### 回退函数

在`NFTSwap`中，用户使用`ETH`购买`NFT`。因此，合约需要实现`fallback()`函数来接收`ETH`。

        fallback() external payable{}
    

### onERC721Received

`ERC721`的安全转账函数会检查接收合约是否实现了`onERC721Received()`函数，并返回正确的选择器`selector`。用户下单之后，需要将`NFT`发送给`NFTSwap`合约。因此`NFTSwap`继承`IERC721Receiver`接口，并实现`onERC721Received()`函数：

    contract NFTSwap is IERC721Receiver{
    
        // 实现{IERC721Receiver}的onERC721Received，能够接收ERC721代币
        function onERC721Received(
            address operator,
            address from,
            uint tokenId,
            bytes calldata data
        ) external override returns (bytes4){
            return IERC721Receiver.onERC721Received.selector;
        }
    

### 交易

合约实现了`4`个交易相关的函数：

*   挂单`list()`：卖家创建`NFT`并创建订单，并释放`List`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`，挂单价格`_price`（**注意：单位是**`wei`）。成功后，`NFT`会从卖家转到`NFTSwap`合约中。
    

        // 挂单: 卖家上架NFT，合约地址为_nftAddr，tokenId为_tokenId，价格_price为以太坊（单位是wei）
        function list(address _nftAddr, uint256 _tokenId, uint256 _price) public{
            IERC721 _nft = IERC721(_nftAddr); // 声明IERC721接口合约变量
            require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // 合约得到授权
            require(_price > 0); // 价格大于0
    
            Order storage _order = nftList[_nftAddr][_tokenId]; //设置NF持有人和价格
            _order.owner = msg.sender;
            _order.price = _price;
            // 将NFT转账到合约
            _nft.safeTransferFrom(msg.sender, address(this), _tokenId);
    
            // 释放List事件
            emit List(msg.sender, _nftAddr, _tokenId, _price);
        }
    

*   撤单`revoke()`：卖家撤回挂单，并释放`Revoke`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`。成功后，`NFT`会从`NFTSwap`合约转回卖家。
    

        // 撤单： 卖家取消挂单
        function revoke(address _nftAddr, uint256 _tokenId) public {
            Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order        
            require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起
            // 声明IERC721接口合约变量
            IERC721 _nft = IERC721(_nftAddr);
            require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中
            
            // 将NFT转给卖家
            _nft.safeTransferFrom(address(this), msg.sender, _tokenId);
             delete nftList[_nftAddr][_tokenId]; // 删除order
          
            // 释放Revoke事件
            emit Revoke(msg.sender, _nftAddr, _tokenId);
        }
    

*   修改价格`update()`：卖家修改`NFT`订单价格，并释放`Update`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`，更新后的挂单价格`_newPrice`（**注意：单位是**`wei`）。
    

        // 调整价格: 卖家调整挂单价格
        function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public {
            require(_newPrice > 0, "Invalid Price"); // NFT价格大于0
            Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order        
            require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起
            // 声明IERC721接口合约变量
            IERC721 _nft = IERC721(_nftAddr);
            require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中
            
            // 调整NFT价格
            _order.price = _newPrice;
          
            // 释放Update事件
            emit Update(msg.sender, _nftAddr, _tokenId, _newPrice);
        }
    

*   购买`purchase`：买家支付`ETH`购买挂单的`NFT`，并释放`Purchase`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`。成功后，`ETH`将转给卖家，`NFT`将从`NFTSwap`合约转给买家。
    

        // 购买: 买家购买NFT，合约为_nftAddr，tokenId为_tokenId，调用函数时要附带ETH
        function purchase(address _nftAddr, uint256 _tokenId) payable public {
            Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order        
            require(_order.price > 0, "Invalid Price"); // NFT价格大于0
            require(msg.value >= _order.price, "Increase price"); // 购买价格大于标价
            // 声明IERC721接口合约变量
            IERC721 _nft = IERC721(_nftAddr);
            require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中
    
            // 将NFT转给买家
            _nft.safeTransferFrom(address(this), msg.sender, _tokenId);
            // 将ETH转给卖家，多余ETH给买家退款
            payable(_order.owner).transfer(_order.price);
            payable(msg.sender).transfer(msg.value-_order.price);
    
            delete nftList[_nftAddr][_tokenId]; // 删除order
    
            // 释放Purchase事件
            emit List(msg.sender, _nftAddr, _tokenId, msg.value);
        }
    

总结
--

这一讲，我们建立了一个零手续费的去中心化`NFT`交易所。`OpenSea`虽然对`NFT`的发展做了很大贡献，但它的缺点也非常明显：高手续费、不发币回馈用户、交易机制容易被钓鱼导致用户资产丢失。目前`Looksrare`和`dydx`等新的`NFT`交易平台正在挑战`OpenSea`的位置，`Uniswap`也在研究新的`NFT`交易所。相信不久的将来，我们会用到更好的`NFT`交易所。

---

*Originally published on [0xAA](https://paragraph.com/@wtfacademy/wtf-solidity-38-nft)*
