news 2026/9/15 19:20:36

WTF-Solidity 第38讲:用智能合约搭建零手续费的去中心化 NFT 交易所 NFTSwap

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WTF-Solidity 第38讲:用智能合约搭建零手续费的去中心化 NFT 交易所 NFTSwap

WTF-Solidity 第38讲:用智能合约搭建零手续费的去中心化 NFT 交易所 NFTSwap

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

本篇技术指南完整讲解 WTF-Solidity 教程第 38 讲的核心内容:如何在以太坊上用 Solidity 智能合约实现一个去中心化、零手续费的 NFT 交易所NFTSwap。全文以 Languages/pt-br/38_NFTSwap/readme.md 为骨架,结合仓库内的 NFTSwap.sol 源码、ERC721 系列合约 与 WTFApe 示例 NFT,从设计逻辑、合约实现、底层原理到 Remix 全流程实操逐一展开。读完后你将掌握:如何用事件(Event)、结构体(Struct)、映射(Mapping)与 ERC721 安全转账机制构建一个完整的链上订单簿式 NFT 交易合约,并能在 Remix 中亲手完成部署、授权、挂单、改价、撤单与购买。

背景:为什么需要去中心化 NFT 交易所

原教程以OpenSea为例引出问题:OpenSea是以太坊上最大的 NFT 交易平台,总交易量超过 300 亿美元,且对每笔交易抽成 2.5%,其运作并非去中心化,用户承担了高昂的交易成本。原教程据此提出用智能合约搭建一个零手续费的去中心化 NFT 交易所NFTSwap,让挂单、撤单、改价、购买全部在链上公开执行,无需信任任何平台方。

说明:上述市场规模与费率数据来自原教程的背景叙述,用于引出"零手续费去中心化交易所"的设计动机;本文后续所有技术结论均以仓库中的合约源码为直接依据。

设计逻辑

NFTSwap是一个典型的**链上订单簿(On-chain Order Book)**模型,只有三种参与角色:

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

该设计直接对应合约中的四个行为:list/revoke/update/purchase,合约也用四个事件分别记录这四个行为。

NFTSwap合约源码解析

合约完整源码位于 Languages/pt-br/38_NFTSwap/NFTSwap.sol(根目录下另有同名的 38_NFTSwap/NFTSwap.sol),编译版本为pragma solidity ^0.8.34。它继承了IERC721Receiver接口,以便通过 ERC721 的安全转账机制接收 NFT。

1. 事件:四个行为一一对应

合约包含 4 个事件,参数均使用indexed关键字标注可检索字段,方便链下索引与前端监听:

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 );
  • List:挂单成功时触发,记录卖家、NFT 合约地址、tokenId与挂单价格;
  • Purchase:购买成交时触发,记录买家、NFT 合约地址、tokenId与成交价;
  • Revoke:撤单成功时触发,记录卖家、NFT 合约地址与tokenId
  • Update:改价成功时触发,记录卖家、NFT 合约地址、tokenId与新的挂单价格newPrice

事件是 Solidity 中合约与链下世界通信的主要通道,indexed参数可被日志高效检索,这正是交易所需要"上架/成交"通知能力的基础。

2. 订单:Order结构体 + 双重nftList映射

NFT 订单被抽象为一个Order结构体,只包含两个字段——持有人owner与挂单价格price

struct Order { address owner; uint256 price; } // NFT Order 映射:NFT合约地址 => tokenId => 订单 mapping(address => mapping(uint256 => Order)) public nftList;

nftList是一个双重映射:第一层键是 NFT 合约地址(address),第二层键是tokenIduint256),最终定位到该 NFT 的订单。由于映射天然以键唯一,同一tokenId最多只存在一个订单的设计由数据结构直接保证。映射被声明为public,Solidity 会自动生成对应的 getter 函数,前端可以直接查询任意 NFT 的订单状态(见下文 Remix 实操第 5 步)。

当订单成交(purchase)或撤单(revoke)时,源码使用delete nftList[_nftAddr][_tokenId]将订单信息整体清零(owner归零地址、price归 0),符合"订单信息清零"的设计约定。

3. 回退函数:接收 ETH

NFTSwap中,用户使用ETH购买 NFT,因此合约需要实现接收 ETH 的能力:

fallback() external payable {}

fallback()被声明为payable,保证任何没有匹配到现有函数的 ETH 转账都能被合约接收(不会被 revert 拒绝)。关于 ETH 转账的三种方式(transfer/send/call),可参考仓库 20_SendETH 一讲的讲解。

4.onERC721Received:让合约能"安全地"接收 NFT

ERC721 的安全转账safeTransferFrom)会在转账时检查接收方:如果接收方是合约,则必须实现onERC721Received()函数并返回正确的选择器selector,否则转账会被拒绝,从而防止 NFT 被永久锁死在无法转出的合约里。

用户挂单后需要把 NFT 发送到NFTSwap合约托管,所以NFTSwap继承IERC721Receiver接口并实现该函数:

contract NFTSwap is IERC721Receiver { // 实现 IERC721Receiver.onERC721Received,使合约能够接收 ERC721 代币 function onERC721Received( address, address, uint256, bytes calldata ) external pure override returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } }

接口定义位于 34_ERC721/IERC721Receiver.sol。只要返回0x150b7a02(即IERC721Receiver.onERC721Received.selector),就表示合约明确知晓并接受 ERC721 协议的接收约定。

5. 四个交易函数

合约实现了 4 个交易相关函数,覆盖完整的买卖生命周期。以下代码为仓库最新源码(与原文档教学代码逻辑一致,并在此基础上增加了若干安全校验,差异点会在小节末尾说明)。

挂单list()

卖家上架 NFT 并创建订单,成功后 NFT 从卖家转入NFTSwap合约托管:

function list(address _nftAddr, uint256 _tokenId, uint256 _price) public { require(_price > 0, "Price must be greater than 0"); IERC721 _nft = IERC721(_nftAddr); require( _nft.ownerOf(_tokenId) == msg.sender, "Only the owner can list the NFT" ); require( _nft.getApproved(_tokenId) == address(this), "Contract is not approved to transfer this NFT" ); nftList[_nftAddr][_tokenId] = Order(msg.sender, _price); _nft.safeTransferFrom(msg.sender, address(this), _tokenId); emit List(msg.sender, _nftAddr, _tokenId, _price); }

参数说明:

  • _nftAddr:NFT 合约地址;
  • _tokenId:NFT 对应的tokenId
  • _price:挂单价格,单位是wei(1 ether = 10^18 wei)。

核心校验与动作:

  1. _price > 0:价格必须大于 0;
  2. _nft.ownerOf(_tokenId) == msg.sender:只有 NFT 实际持有人才能挂单(该检查为最新源码新增);
  3. _nft.getApproved(_tokenId) == address(this):卖家必须先调用 NFT 合约的approve()把该tokenId授权给NFTSwap合约,合约才能调用safeTransferFrom完成托管;
  4. safeTransferFrom(msg.sender, address(this), _tokenId):将 NFT 安全转入合约;
  5. 释放List事件。
撤单revoke()

卖家撤销挂单,NFT 从合约转回卖家,订单信息被清零:

function revoke(address _nftAddr, uint256 _tokenId) public { Order memory order = nftList[_nftAddr][_tokenId]; require(order.owner == msg.sender, "You are not the owner of this NFT"); IERC721 _nft = IERC721(_nftAddr); require( _nft.ownerOf(_tokenId) == address(this), "NFT is not in the contract" ); delete nftList[_nftAddr][_tokenId]; _nft.safeTransferFrom(address(this), msg.sender, _tokenId); emit Revoke(msg.sender, _nftAddr, _tokenId); }

核心校验与动作:

  1. order.owner == msg.sender:只有挂单的卖家本人可以撤单;
  2. _nft.ownerOf(_tokenId) == address(this):确认 NFT 确实托管在合约中(订单有效);
  3. delete订单(Effects),再执行转账(Interactions);
  4. 释放Revoke事件。
修改价格update()

卖家修改挂单价格,只更新price字段,不转移 NFT:

function update( address _nftAddr, uint256 _tokenId, uint256 _newPrice ) public { require(_newPrice > 0, "Price must be greater than 0"); Order storage order = nftList[_nftAddr][_tokenId]; require(order.owner == msg.sender, "You are not the owner of this NFT"); order.price = _newPrice; emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); }

参数_newPrice为新的挂单价格(单位wei)。注意这里使用了storage引用直接改写链上订单的price字段,比重新写入整个结构体更省 gas。

购买purchase()

买家支付ETH购买 NFT,成功后 ETH 转给卖家、NFT 转给买家、多余 ETH 退回买家、订单清零:

function purchase(address _nftAddr, uint256 _tokenId) public payable { Order memory order = nftList[_nftAddr][_tokenId]; require(order.price > 0, "NFT is not listed for sale"); require(msg.value >= order.price, "Insufficient ETH to purchase NFT"); IERC721 _nft = IERC721(_nftAddr); require( _nft.ownerOf(_tokenId) == address(this), "NFT is not in the contract" ); delete nftList[_nftAddr][_tokenId]; _nft.safeTransferFrom(address(this), msg.sender, _tokenId); payable(order.owner).transfer(order.price); if (msg.value > order.price) { payable(msg.sender).transfer(msg.value - order.price); } emit Purchase(msg.sender, _nftAddr, _tokenId, order.price); }

核心校验与动作:

  1. order.price > 0:确认该 NFT 确实处于挂单状态;
  2. msg.value >= order.price:买家支付金额不能低于挂单价;
  3. _nft.ownerOf(_tokenId) == address(this):确认 NFT 托管在合约中;
  4. delete订单(Effects),再完成 NFT 转账与 ETH 结算(Interactions);
  5. payable(_order.owner).transfer(_order.price):把挂单价转给卖家;
  6. if (msg.value > order.price):如果买家多付了,把差额原路退回;
  7. 释放Purchase事件,price参数取订单中的挂单价(而非买家实际支付额)。

教学代码与最新源码的差异

原文档中的教学代码与仓库最新源码逻辑一致,但最新源码 Languages/pt-br/38_NFTSwap/NFTSwap.sol 做了几处值得注意的加强:

  • list()新增_nft.ownerOf(_tokenId) == msg.sender校验,从源头杜绝非持有人冒名挂单;
  • purchase()revoke()均采用Order memory order拷贝读取订单,并先删除订单、再执行转账(即 Checks-Effects-Interactions 顺序),从源码结构看可显著降低重入攻击风险;
  • purchase()的退款逻辑改为if (msg.value > order.price)条件判断,仅在存在差额时才执行退款;
  • 更新后的源码在purchase()使用transfer()发送 ETH(固定 2300 gas),可防止收款合约通过回调消耗过多 gas 进行重入。

底层原理:ERC721 授权与安全转账机制

NFTSwap能运作的前提,是 ERC721 标准的**授权(Approve)安全转账(Safe Transfer)**机制,其实现位于 34_ERC721/ERC721.sol:

  • 授权查询getApproved(tokenId)返回指定tokenId当前的授权地址(ERC721.sol 中通过_tokenApprovals映射实现)。NFTSwap.list()正是靠它校验"合约是否已获得该 NFT 的转移授权"。
  • 授权函数approve(to, tokenId)仅允许 NFT 持有人或被setApprovalForAll批量授权的地址调用(ERC721.sol),这与实操中"卖家先 approve、再 list"的顺序完全吻合。
  • 安全转账safeTransferFrom最终调用私有函数_safeTransfer,内部先执行_transfer完成余额与所有权变更,再调用_checkOnERC721Received检查接收方(ERC721.sol)。若接收方是合约且未实现onERC721Received,转账会被 revert,这正是NFTSwap必须实现onERC721Received()的原因。

Remix 全流程实操

下面按原教程步骤,在 Remix IDE 的 JavaScript VM(测试环境)中完整跑通 NFTSwap 的九步流程。测试 NFT 合约使用 34_ERC721/WTFApe.sol(一个继承自 ERC721.sol 的简单 NFT,总量MAX_APES = 10000,构造函数传入名称与代号,mint可铸造任意tokenId)。

1. 部署 NFT 合约WTFApe

先在 Remix 中编译并部署WTFApe,构造参数NAMESYMBOL均填WTF

随后调用mint给自己铸造两个 NFT(tokenId分别为01),并调用ownerOf(0)确认tokenId = 0的 NFT 持有人是自己。NFT 的铸造与所有权逻辑详见 34_ERC721 一讲。

2. 部署NFTSwap合约

编译并部署 NFTSwap.sol,部署时无需构造参数(合约无构造函数入参)。

3. 授权 NFT 给NFTSwap合约

WTFApe合约中调用approve(address to, uint256 tokenId)

  • to:授权目标地址,填NFTSwap合约地址;
  • tokenId:要授权挂单的 NFT 编号,这里为0

依次为tokenId = 0tokenId = 1完成授权。只有先授权,NFTSwap.list()中的getApproved检查才能通过。

4. 挂单list

调用NFTSwaplist(address _nftAddr, uint256 _tokenId, uint256 _price)

  • _nftAddrWTFApe合约地址;
  • _tokenId:要挂单的 NFT 编号,这里为0
  • _price:挂单价格,此处为1 wei

挂单成功后 NFT 会从卖家地址转入NFTSwap合约。重复一次,以1 wei的价格把tokenId = 1也挂上。

5. 查看挂单

调用公开映射nftList的自动 getter:传入_nftAddr_tokenId,即可查到该 NFT 的订单,返回owner(挂单卖家地址)与price(挂单价格):

返回结果显示owner为卖家地址、price为 1,说明挂单成功且价格正确。

6. 修改价格update

调用update(address _nftAddr, uint256 _tokenId, uint256 _newPrice),把tokenId = 0的价格改为77 wei

  • _nftAddrWTFApe合约地址;
  • _tokenId0
  • _newPrice77(单位 wei)。

改价后再查nftListprice已变为77,说明更新生效。改价只更新链上订单字段,无需再次转移 NFT。

7. 撤单revoke

调用revoke(address _nftAddr, uint256 _tokenId)撤下tokenId = 1的挂单:

  • _nftAddrWTFApe合约地址;
  • _tokenId1

再次查询nftListowner归零地址、price归 0,说明订单已被删除,NFT 已退回卖家。注意:撤单后若要重新挂单,必须重新走"授权 + 挂单"两步。

8. 购买purchase

切换 Remix 账户(用另一个地址充当买家),调用purchase(address _nftAddr, uint256 _tokenId),并在交易面板的Value中填入要支付的 ETH 数量:

  • _nftAddrWTFApe合约地址;
  • _tokenId0(前面已把tokenId = 1撤单,这里买0号);
  • 支付金额:77 wei(须大于等于挂单价)。

成交后:NFT 从合约转入买家,77 wei转给卖家,订单被删除。若支付金额超过挂单价,差额会自动退回买家。

9. 验证所有权变更

WTFApe合约中调用ownerOf(0),确认tokenId = 0的持有人已变为买家地址,即购买成功:

至此,一笔完整的去中心化 NFT 交易(挂单 → 改价 → 撤单 → 购买)在链上全部完成,全程没有平台抽成,也不依赖任何中心化撮合服务器。

安全与设计要点小结

结合合约源码可以总结出几个关键设计要点(部分为基于代码结构的推断,供读者结合文档批判性阅读):

  • 托管式挂单:挂单即把 NFT 锁定在交易所合约中,杜绝了"一物多挂""挂单后私下转移"等链下撮合模式常见的欺诈风险,成交与撤单都必须由合约统一执行;
  • CEI 顺序purchaserevoke均先删除订单、再执行外部转账,配合transfer()的 2300 gas 限制,从源码结构看可有效降低重入攻击面;
  • 单边锁定:同一tokenId最多一个订单,由mapping键唯一性天然保证;
  • 已知局限:作为教学合约,它没有实现版税(Royalty)、批量挂单、出价(Offer)等商业交易所功能;transfer()在部分新链上可能因 gas 语义差异表现不同,生产级实现通常会改用call配合重入防护(如 OpenZeppelin 的ReentrancyGuard)。另外,update()在最新源码中不再校验 NFT 是否仍托管在合约内,从代码结构看,只要订单存在且owner匹配,NFT 一般应处于托管状态,但读者应在二次开发时自行评估该取舍。

总结

本讲用不到 200 行代码实现了一个功能完整的去中心化 NFT 交易所NFTSwap:通过Order结构体 + 双重nftList映射构建链上订单簿,通过四个事件对外广播交易行为,通过onERC721Received+safeTransferFrom安全托管 NFT,通过fallback接收买家支付的 ETH。它证明了"挂单、改价、撤单、购买"这套传统交易所的核心能力,完全可以用智能合约以零手续费、全公开、无托管风险的方式在链上实现。原教程在结论中展望:随着Looksraredydx等新平台和Uniswap等项目的探索,NFT 交易基础设施正在向更开放、更公平的方向演进——而理解NFTSwap的源码,正是理解这一切的起点。

对 ERC721 标准本身还不熟悉的读者,建议先阅读仓库 34_ERC721 一讲的 readme.md;对事件、映射、结构体等 Solidity 基础语法,可回看 07_Mapping 与 12_Event 两讲。

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 19:19:22

如何快速修改AI短剧的镜头和替换包装?

如何快速修改AI短剧的镜头和替换包装?特种猫的做法是正片和包装分层管理:镜头只替换有问题的单个分镜,包装用独立模板轨道一键覆盖,不重跑整集。截至 2026 年,创作者普遍踩3个坑:改1个镜头要整集重新生成、…

作者头像 李华
网站建设 2026/9/15 19:17:47

Unity角色口型同步与眼神模拟:SALSA With RandomEyes实战调优指南

学过几年Unity动画,接手过不少数字人、对话NPC的项目,我敢说在“让角色开口说话”这件事上,最让我省心的方案就是SALSA With RandomEyes。这个插件从名字就能看出来,它干两件事:SALSA负责说话时的口型同步,…

作者头像 李华