Skip to content

Files

Latest commit

f10e8b2 · Jun 12, 2024

History

History
This branch is 117 commits behind AmazingAng/WTF-Solidity:main.

38_NFTSwap

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Aug 9, 2022
Mar 13, 2024
Jun 12, 2024
title tags
38. NFT交易所
solidity
application
wtfacademy
ERC721
NFT Swap

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

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

推特:@0xAA_Science@WTFAcademy_

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity


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合约地址_nftAddrNFT对应的_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合约地址_nftAddrNFT对应的_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合约地址_nftAddrNFT对应的_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合约地址_nftAddrNFT对应的_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 Purchase(msg.sender, _nftAddr, _tokenId, _order.price);
    }

Remix实现

1. 部署NFT合约

参考 ERC721 教程了解NFT,并部署WTFApe NFT合约。

部署NFT合约

将首个NFT mint给自己,这里mint给自己是为了之后能够上架NFT、修改价格等一系类操作。

mint(address to, uint tokenId)方法有2个参数:

to:将 NFT mint给指定的地址,这里通常是自己的钱包地址。

tokenId: WTFApe合约定义了总量为10000个NFT,图中mint它的的第一个和第二个NFT,tokenId分别为01

mint NFT

WTFApe合约中,利用ownerOf确认自己已经获得tokenId为0的NFT。

ownerOf(uint tokenId)方法有1个参数:

tokenId: tokenId为NFT的id,本案例中为上述mint的0Id。

确认自己已经获得NFT

按照上述方法,将TokenId为 01 的NFT都mint给自己,其中tokenId0的,我们执行更新购买操作,tokenId1的,我们执行下架操作。

2. 部署NFTSwap合约

部署NFTSwap合约。

部署NFTSwap合约

3. 将要上架的NFT授权给NFTSwap合约

WTFApe合约中调用 approve()授权函数,将自己持有的tokenId为0的NFT授权给NFTSwap合约地址。

approve(address to, uint tokenId)方法有2个参数:

to: 将tokenId授权给 to 地址,本案例中将授权给NFTSwap合约地址。

tokenId: tokenId为NFT的id,本案例中为上述mint的0Id。

按照上述方法,同理将tokenId1的NFT也授权给NFTSwap合约地址。

4. 上架NFT

调用NFTSwap合约的list()函数,将自己持有的tokenId为0的NFT上架到NFTSwap,价格设为1 wei

list(address _nftAddr, uint256 _tokenId, uint256 _price)方法有3个参数:

_nftAddr: _nftAddr为NFT合约地址,本案例中为WTFApe合约地址。

_tokenId: _tokenId为NFT的id,本案例中为上述mint的0Id。

_price: _price为NFT的价格,本案例中为1 wei

按照上述方法,同理将自己持有的tokenId为1的NFT上架到NFTSwap,价格设为1 wei

5. 查看上架NFT

调用NFTSwap合约的nftList()函数查看上架的NFT。

nftList:是一个NFT Order的映射,结构如下:

nftList[_nftAddr][_tokenId]: 输入_nftAddr_tokenId,返回一个NFT订单。

6. 更新NFT价格

调用NFTSwap合约的update()函数,将tokenId为0的NFT价格更新为77 wei

update(address _nftAddr, uint256 _tokenId, uint256 _newPrice)方法有3个参数:

_nftAddr: _nftAddr为NFT合约地址,本案例中为WTFApe合约地址。

_tokenId: _tokenId为NFT的id,本案例中为上述mint的0Id。

_newPrice: _newPrice为NFT的新价格,本案例中为77 wei

执行update之后,调用nftList 查看更新后的价格

5. 下架NFT

调用NFTSwap合约的revoke()函数下架NFT。

上述文章中,我们上架了2个NFT,tokenId分别为 01。本次方法中,我们下架tokenId1的NFT。

revoke(address _nftAddr, uint256 _tokenId)方法有2个参数:

_nftAddr: _nftAddr为NFT合约地址,本案例中为WTFApe合约地址。

_tokenId: _tokenId为NFT的id,本案例中为上述mint的1Id。

调用NFTSwap合约的nftList()函数,可以看到NFT已经下架。再次上架需要重新授权。

注意下架NFT之后,需要重新从步骤3开始,重新授权和上架NFT之后,才能进行购买

6. 购买NFT

切换账号,调用NFTSwap合约的purchase()函数购买NFT,购买时需要输入NFT合约地址,tokenId,并输入支付的ETH

我们下架了tokenId1的NFT,现在还存在tokenId0的NFT,所以我们可以购买tokenId0的NFT。

purchase(address _nftAddr, uint256 _tokenId, uint256 _wei)方法有3个参数:

_nftAddr: _nftAddr为NFT合约地址,本案例中为WTFApe合约地址。

_tokenId: _tokenId为NFT的id,本案例中为上述mint的0Id。

_wei: _wei为支付的ETH数量,本案例中为77 wei

7. 验证NFT持有人改变

购买成功之后,调用WTFApe合约的ownerOf()函数,可以看到NFT持有者发生变化,购买成功!

总结

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