尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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 合约地址与tokenIdUpdate改价成功时触发记录卖家、NFT 合约地址、tokenId与新的挂单价格newPrice。事件是 Solidity 中合约与链下世界通信的主要通道indexed参数可被日志高效检索这正是交易所需要上架/成交通知能力的基础。2. 订单Order结构体 双重nftList映射NFT 订单被抽象为一个Order结构体只包含两个字段——持有人owner与挂单价格pricestruct Order { address owner; uint256 price; } // NFT Order 映射NFT合约地址 tokenId 订单 mapping(address mapping(uint256 Order)) public nftList;nftList是一个双重映射第一层键是 NFT 合约地址address第二层键是tokenIduint256最终定位到该 NFT 的订单。由于映射天然以键唯一同一tokenId最多只存在一个订单的设计由数据结构直接保证。映射被声明为publicSolidity 会自动生成对应的 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让合约能安全地接收 NFTERC721 的安全转账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); }参数说明_nftAddrNFT 合约地址_tokenIdNFT 对应的tokenId_price挂单价格单位是wei1 ether 10^18 wei。核心校验与动作_price 0价格必须大于 0_nft.ownerOf(_tokenId) msg.sender只有 NFT 实际持有人才能挂单该检查为最新源码新增_nft.getApproved(_tokenId) address(this)卖家必须先调用 NFT 合约的approve()把该tokenId授权给NFTSwap合约合约才能调用safeTransferFrom完成托管safeTransferFrom(msg.sender, address(this), _tokenId)将 NFT 安全转入合约释放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); }核心校验与动作order.owner msg.sender只有挂单的卖家本人可以撤单_nft.ownerOf(_tokenId) address(this)确认 NFT 确实托管在合约中订单有效先delete订单Effects再执行转账Interactions释放Revoke事件。修改价格update()卖家修改挂单价格只更新price字段不转移 NFTfunction 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); }核心校验与动作order.price 0确认该 NFT 确实处于挂单状态msg.value order.price买家支付金额不能低于挂单价_nft.ownerOf(_tokenId) address(this)确认 NFT 托管在合约中先delete订单Effects再完成 NFT 转账与 ETH 结算Interactionspayable(_order.owner).transfer(_order.price)把挂单价转给卖家if (msg.value order.price)如果买家多付了把差额原路退回释放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构造参数NAME、SYMBOL均填WTF随后调用mint给自己铸造两个 NFTtokenId分别为0和1并调用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 0和tokenId 1完成授权。只有先授权NFTSwap.list()中的getApproved检查才能通过。4. 挂单list调用NFTSwap的list(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 顺序purchase与revoke均先删除订单、再执行外部转账配合transfer()的 2300 gas 限制从源码结构看可有效降低重入攻击面单边锁定同一tokenId最多一个订单由mapping键唯一性天然保证已知局限作为教学合约它没有实现版税Royalty、批量挂单、出价Offer等商业交易所功能transfer()在部分新链上可能因 gas 语义差异表现不同生产级实现通常会改用call配合重入防护如 OpenZeppelin 的ReentrancyGuard。另外update()在最新源码中不再校验 NFT 是否仍托管在合约内从代码结构看只要订单存在且owner匹配NFT 一般应处于托管状态但读者应在二次开发时自行评估该取舍。总结本讲用不到 200 行代码实现了一个功能完整的去中心化 NFT 交易所NFTSwap通过Order结构体 双重nftList映射构建链上订单簿通过四个事件对外广播交易行为通过onERC721ReceivedsafeTransferFrom安全托管 NFT通过fallback接收买家支付的 ETH。它证明了挂单、改价、撤单、购买这套传统交易所的核心能力完全可以用智能合约以零手续费、全公开、无托管风险的方式在链上实现。原教程在结论中展望随着Looksrare、dydx等新平台和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),仅供参考
返回列表