React Native DApp 開(kāi)發(fā)全棧實(shí)戰(zhàn)·從 0 到 1 系列(NFT交易所-合約部分)

前言

本文以 OpenZeppelin 5.x 最新組件為基礎(chǔ),用 Hardhat 完成「合約 → 編譯 → 測(cè)試 → 部署」全鏈路流程。解決 openzeppelin V5 在 0.8.24 環(huán)境下易出現(xiàn)的編譯失敗的解決方案;示范了零托管的現(xiàn)場(chǎng)分賬邏輯:版稅、平臺(tái)費(fèi)、賣家收益一次性鏈上清算,合約不留余額,安全又省 Gas。

前期準(zhǔn)備

  • 啟動(dòng)本地節(jié)點(diǎn)npx hardhat node
  • 想要快速驗(yàn)證合約,可以把合約放在Remix IDE,節(jié)省環(huán)境配置環(huán)節(jié)

合約編譯

  • 特別說(shuō)明針對(duì)代碼中使用openzeppelin V5會(huì)出現(xiàn)編譯失敗問(wèn)題
  • 解決方法在hardhat.config.json進(jìn)行以下設(shè)置
module.exports = {
  solidity:{
   version: "0.8.24",
    settings: {
      evmVersion: "cancun",//處理問(wèn)題的關(guān)鍵代碼
    }

  },
  //其他設(shè)置......
  }

核心代碼

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/*
  OpenZeppelin ^5.4.0
  無(wú)需 PullPayment / EscrowPayment
*/

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract OZMarketplace is
    ReentrancyGuardTransient,
    Pausable,
    Ownable
{
    /* ===== 事件 ===== */
    event Listed(uint256 indexed tokenId, address indexed seller, uint256 price);
    event Delisted(uint256 indexed tokenId);
    event Sold(uint256 indexed tokenId, address buyer, address seller, uint256 price);

    /* ===== 結(jié)構(gòu) ===== */
    struct Listing {
        address seller;
        uint256 price;
    }

    /* ===== 常量 & 狀態(tài) ===== */
    IERC721 public immutable NFT;
    uint96  public feeBps = 250;        // 2.5 %
    uint96  public constant FEE_DENOMINATOR = 10_000;

    mapping(uint256 tokenId => Listing) private _listings;

    constructor(address _nft, address initialOwner) Ownable(initialOwner) {
        NFT = IERC721(_nft);
    }

    /* ===== 管理函數(shù) ===== */
    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function setFeeBps(uint96 _feeBps) external onlyOwner {
        require(_feeBps <= FEE_DENOMINATOR, "fee > 100%");
        feeBps = _feeBps;
    }

    /* ===== 掛單 ===== */
    function list(uint256 tokenId, uint256 price) external whenNotPaused {
        require(price > 0, "Price zero");
        require(NFT.ownerOf(tokenId) == msg.sender, "Not owner");
        require(
            NFT.isApprovedForAll(msg.sender, address(this)) ||
            NFT.getApproved(tokenId) == address(this),
            "Not approved"
        );
        require(_listings[tokenId].seller == address(0), "Listed");

        _listings[tokenId] = Listing(msg.sender, price);
        emit Listed(tokenId, msg.sender, price);
    }

    /* ===== 撤單 ===== */
    function delist(uint256 tokenId) external whenNotPaused {
        Listing memory l = _listings[tokenId];
        require(l.seller == msg.sender, "Not seller");
        delete _listings[tokenId];
        emit Delisted(tokenId);
    }

    /* ===== 購(gòu)買(現(xiàn)場(chǎng)轉(zhuǎn)賬,無(wú)托管) ===== */
    function buy(uint256 tokenId)
        external
        payable
        whenNotPaused
        nonReentrant
    {
        Listing memory l = _listings[tokenId];
        require(l.seller != address(0), "Not listed");
        require(msg.value == l.price, "Wrong value");

        delete _listings[tokenId];

        /* 版稅 & 手續(xù)費(fèi)計(jì)算 */
        (address royaltyReceiver, uint256 royaltyAmount) =
            ERC2981(address(NFT)).royaltyInfo(tokenId, l.price);

        uint256 feeAmount = (l.price * feeBps) / FEE_DENOMINATOR;
        uint256 sellerAmount = l.price - royaltyAmount - feeAmount;

        /* 現(xiàn)場(chǎng)轉(zhuǎn)賬 —— 合約不留余額 */
        if (royaltyAmount > 0) {
            (bool okRoyalty, ) = royaltyReceiver.call{value: royaltyAmount}("");
            require(okRoyalty, "Royalty fail");
        }
        if (feeAmount > 0) {
            (bool okFee, ) = owner().call{value: feeAmount}("");
            require(okFee, "Fee fail");
        }
        (bool okSeller, ) = l.seller.call{value: sellerAmount}("");
        require(okSeller, "Seller fail");

        NFT.transferFrom(l.seller, msg.sender, tokenId);
        emit Sold(tokenId, msg.sender, l.seller, l.price);
    }

    /* ===== 查詢 ===== */
    function getListing(uint256 tokenId)
        external
        view
        returns (address seller, uint256 price)
    {
        Listing memory l = _listings[tokenId];
        return (l.seller, l.price);
    }
}

合約編譯npx hardat compile

合約測(cè)試

測(cè)試流程說(shuō)明

  • 完整掛單-購(gòu)買流程

  • NFT 歸屬:addr1 → addr2

  • 資金:

    • 版稅 5% → deployer
    • 平臺(tái)費(fèi) 2.5% → deployer
    • 剩余 92.5% → addr1

測(cè)試代碼

const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("OZMarketplace",function(){
    let SimpleClosedNFT;//合約
    let OZMarketplace;//合約
    let addr1;
    let addr2;
    let firstAccount//第一個(gè)賬戶
    let secondAccount//第二個(gè)賬戶
    // let mekadate='ipfs://QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB';
    // let mekadate1="ipfs://QmXzbsbjpWpbSGJkgGzmk6r6HLz1nvjpEtjFR6bVhMh3U9"
    beforeEach(async function(){
        await deployments.fixture(["SimpleClosedNFT","OZMarketplace"]);
        [addr1,addr2]=await ethers.getSigners();
        firstAccount=(await getNamedAccounts()).firstAccount;
        secondAccount=(await getNamedAccounts()).secondAccount;
        const SimpleClosedNFTDeployment = await deployments.get("SimpleClosedNFT");
        SimpleClosedNFT = await ethers.getContractAt("SimpleClosedNFT",SimpleClosedNFTDeployment.address);//已經(jīng)部署的合約交互
        const OZMarketplaceDeployment = await deployments.get("OZMarketplace");
        OZMarketplace = await ethers.getContractAt("OZMarketplace",OZMarketplaceDeployment.address);//已經(jīng)部署的合約交互
    })
    describe("OZMarketplace",function(){
        it("把nft掛單-購(gòu)買流程",async ()=>{

            //鑄造一個(gè)nft
            const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
            const price = ethers.parseEther("0.1");
            const royaltyBps=500;
            await SimpleClosedNFT.connect(addr1).create(uri,price,royaltyBps,{value:price})
            console.log(await SimpleClosedNFT.tokenURI(1))
           console.log("nft的所有者",await SimpleClosedNFT.ownerOf(1))
           const mintPrice=await SimpleClosedNFT.mintPrice(1)
           console.log("nft的價(jià)格",ethers.formatEther(mintPrice))
           //授權(quán)把鑄造的nft授權(quán)給交易所
           await SimpleClosedNFT.connect(addr1).approve(await OZMarketplace.getAddress(), 1);
           console.log("授權(quán)成功",await SimpleClosedNFT.getApproved(1))
           //掛單
           await OZMarketplace.connect(addr1).list(1,price)
             const [seller, pricew] = await OZMarketplace.getListing(1);
             console.log("賣方:",seller)
             console.log("價(jià)格:",ethers.formatEther(pricew))
           //購(gòu)買
             const tx=await OZMarketplace.connect(addr2).buy(1,{value:price})
             const receipt = await tx.wait();
             console.log("購(gòu)買成功","gas消耗:",receipt.gasUsed , "gas價(jià)格:",receipt.gasPrice)
             console.log("nft的所有者",await SimpleClosedNFT.ownerOf(1))
             //校驗(yàn)下架 addr2購(gòu)買了所欲 下架了
             const [seller1, pricew1] = await OZMarketplace.getListing(1);
             console.log("賣方:",seller1)
             console.log("價(jià)格:",ethers.formatEther(pricew1))
            //*  校驗(yàn)資金分賬 */
            const gasUsed = receipt.gasUsed * receipt.gasPrice;
            console.log("gas消耗:",gasUsed)
            //版稅
            const royaltyAmount = price*500n / 10_000n
            console.log("版稅:",ethers.formatEther(royaltyAmount))
            //平臺(tái)
            const feeAmount = price * 250n / 10_000n;
            console.log("平臺(tái):",ethers.formatEther(feeAmount))

            //addr1收入
            const addr1Amount = price - royaltyAmount - feeAmount;
            console.log("addr1收入:",ethers.formatEther(addr1Amount))
        })
    })
})

測(cè)試指令npx hardhat test ./test/xxx,js

合約部署

部署代幣

module.exports = async  ({getNamedAccounts,deployments})=>{
    const getNamedAccount = (await getNamedAccounts()).firstAccount;
    
    const MyNFT=await deployments.get("SimpleClosedNFT");//獲取nft合約

    const {deploy,log} = deployments;
    const OZMarketplace=await deploy("OZMarketplace",{
        from:getNamedAccount,
        args: [MyNFT.address,getNamedAccount],//參數(shù) nft合約地址,所有者
        log: true,
    })
    // await hre.run("verify:verify", {
    //     address: TokenC.address,
    //     constructorArguments: [TokenName, TokenSymbol],
    //     });
    console.log('OZMarketplace合約地址',OZMarketplace.address)
}
module.exports.tags = ["all", "OZMarketplace"];
  • 編譯指令npx hardat deploy --tags xxx1,xxx2;
  • 參數(shù)說(shuō)明例如 NFT,Exchange) 分別部署nft合約和nft交易的合約

總結(jié)

至此NFT交易合約全部完成

  1. 環(huán)境踩坑:用 "evmVersion": "cancun" 秒解 OZ V5 與 Solidity 0.8.24 的編譯沖突。
  2. 合約設(shè)計(jì):僅用 100 行代碼實(shí)現(xiàn)掛單、撤單、購(gòu)買、暫停、手續(xù)費(fèi)及 ERC-2981 版稅自動(dòng)分賬,gas 優(yōu)化到極限。
  3. 全流程測(cè)試:Hardhat 本地網(wǎng)模擬鑄造 → 授權(quán) → 掛單 → 購(gòu)買 → 資金拆分,驗(yàn)證 NFT 所有權(quán)與資金流向 100% 正確。
  4. 一鍵部署:Hardhat-deploy 腳本化部署,tags 機(jī)制可組合部署 NFT 與 Marketplace,后續(xù)可直接 verify 上鏈。

現(xiàn)在,你可以把合約地址塞進(jìn)前端,或者繼續(xù)拓展:支持 ERC-1155、批量掛單、鏈下簽名訂單、實(shí)時(shí)事件推送。一個(gè)生產(chǎn)級(jí)的 NFT 市場(chǎng)雛形,正式就緒!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容