如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

fffmCQ.jpg

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

在Web3世界,想必没有人不知道Opensea的大名,它是以太坊上最大的NFT交易平台,累积交易量达到了300亿美元。但是,Opensea并不是去中心化的交易所,它是一个公司,赚的钱也不会发给用户。要知道,Opnesea的交易抽成比例达到2.5%,可以说非常高了。也正因为其中心化的特点,很多玩家逐渐转型其他的新型NFT交易平台,比如LooksRare、Rarible等等。

不过作为一个区块链开发,我们为什么不尝试自己构建一个NFT交易所DAPP?一个去中心化的NFTSwap。说干就干,接下来我们使用Solidity智能合约的逻辑,构建一个完全开源、没有手续费的NFT交易所DAPP。

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

NFT交易所DAPP需求逻辑

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

NFT交易所DAPP合约撰写

事件

合约包含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;

回退函数

在NFT交易所中中,用户使用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 Purchase(msg.sender, _nftAddr, _tokenId, msg.value);
    }

NFT交易所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分别为0和1。

mint NFT

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

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

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

确认自己已经获得NFT

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

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。

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

按照上述方法,同理将tokenId为1的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。

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

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

5. 查看上架NFT

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

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

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

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

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 查看更新后的价格

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

5. 下架NFT

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

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

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

  • _nftAddr: _nftAddr为NFT合约地址,本案例中为WTFApe合约地址。
  • _tokenId: _tokenId为NFT的id,本案例中为上述mint的1Id。

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

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

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

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

6. 购买NFT

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

我们下架了tokenId为1的NFT,现在还存在tokenId为0的NFT,所以我们可以购买tokenId为0的NFT。

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

  • _nftAddr: _nftAddr为NFT合约地址,本案例中为WTFApe合约地址。
  • _tokenId: _tokenId为NFT的id,本案例中为上述mint的0Id。
  • _wei: _wei为支付的ETH数量,本案例中为1 wei。

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

7. 验证NFT持有人改变

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

如何用智能合约构建NFT交易所?Solidity代码公开及开发教程

总结

按照上述方案,我们就可以在以太坊公链上搭建一个完全去中心化的NFT交易所DAPP,大家开发完之后,提交到各大钱包,这个DAPP就能上架。如果不放心,也可以找审计公司审计一下代码就行。只要会在以太坊链上开发,那么币安链、OK链等等,就都没问题了。

值得注意的是, Opensea本身作为一家中心化的机构,拥有较高的手续费,以致Looksrare和dydx等新的交易平台正在逐渐挑战其领导地位。而且从未来的趋势来看,去中心化的NFTSwap肯定是主流。因此,学会开发去中心化NFT交易所是非常有价值的。如果您也想开发一个这样的DAPP,可以随时联系交流。WeChat微信:btc6540,电报号:@btc6540

声明:该文观点仅代表作者本人,与炒币网无关。炒币网系信息发布平台,仅提供信息存储空间服务。对所包含内容的准确性、可靠性或者完整性不提供任何明示或暗示的保证,并不对文章观点负责。 提示:投资有风险,入市须谨慎。本资讯仅供参阅,不作为投资理财建议。

发表评论

登录后才能评论