资讯动态

WTF-Solidity 第 38 讲:手写零手续费去中心化 NFT 交易所 NFTSwap

发布时间:2026/9/14 17:45:04 来源:尧图企业网站定制
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卖家可以挂单、撤单、改价买家可以付 ETH 买下 NFT全程无平台抽成。通过本文读者将掌握 ERC721 安全转账safeTransferFrom、授权approve/getApproved、IERC721Receiver回调接收机制以及一套可复制的链上订单簿设计思路并能在 Remix 中完整跑通铸造 → 授权 → 挂单 → 改价 → 撤单 → 购买 → 验证所有权全流程。为什么需要链上 NFT 交易所传统中心化 NFT 交易平台是链外撮合、链上结算的存在明显痛点平台方收取高额交易手续费、运营并不去中心化、用户得不到任何回报且站外的钓鱼式交易引导容易导致用户资产损失。基于智能合约的去中心化 NFT 交易所则把挂单、成交、结算全部放到链上由合约代码保证资金与资产的安全流转且天然零平台费用。本讲实现的NFTSwap就是这样一个最小可用的链上交易所完整代码见 38_NFTSwap/NFTSwap.sol。设计逻辑三类角色与一种订单交易所的业务模型很简单围绕三个参与者展开卖家Seller可以挂单上架 NFT、撤销挂单、修改挂单价格买家Buyer支付 ETH 购买挂单中的 NFT订单Order卖家在链上发布的 NFT 订单记录挂单价格与卖家地址。同一个 NFT 系列下同一tokenId最多只能存在一个订单订单成交或撤销后记录被清空。这种一币一单同一tokenId同时最多一个有效挂单的设计让订单簿数据结构非常轻量只需一个嵌套 mapping 即可支撑全量查询。NFTSwap 合约源码解析NFTSwap合约继承IERC721Receiver接口编译环境为pragma solidity ^0.8.34从 34_ERC721/IERC721.sol 与 34_ERC721/IERC721Receiver.sol 导入所需接口。事件链上可检索的交易日志合约定义了四个事件分别对应挂单List、购买Purchase、撤单Revoke、改价Update四类动作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);四个事件的关键字段seller/buyer/nftAddr/tokenId都声明为indexed意味着它们会被写入事件日志的 topics 区可被索引器高效检索——前端无需遍历区块即可按地址或 tokenId 订阅到交易行情。订单结构与订单簿映射Order结构体只有两个字段owner卖家/挂单发起人与price挂单价格单位 wei。nftList是一个双层映射以 NFT 合约地址与tokenId作为联合键定位订单struct Order{ address owner; uint256 price; } // NFT Order映射 mapping(address mapping(uint256 Order)) public nftList;由于nftList声明为public编译器会自动生成对应的 getter外部可调用nftList(_nftAddr, _tokenId)查询任意订单的持有人与价格。fallback让合约能收 ETH买家是用 ETH 购买 NFT 的因此交易所合约必须能接收 ETH。NFTSwap声明了一个payable的 fallback 函数fallback() external payable{}任何向合约地址直接转账且未命中现有函数签名的调用都会进入该 fallback 而被接受合约余额即交易所的资金池。onERC721Received安全接收 ERC721 的通行证ERC721 的safeTransferFrom在把 NFT 转给合约地址时会检查接收方是否实现了onERC721Received()并返回正确的选择器否则回滚整个交易防止 NFT 被永久锁死在黑洞合约里。查看 34_ERC721/ERC721.sol 中的_checkOnERC721Received实现可以看到这一机制当to.code.length 0目标为合约时通过try IERC721Receiver(to).onERC721Received(...)调用回调若返回值不等于IERC721Receiver.onERC721Received.selector则revert ERC721InvalidReceiver(to)。由于卖家挂单后 NFT 会被safeTransferFrom转入NFTSwap合约交易所必须实现该回调function onERC721Received( address operator, address from, uint tokenId, bytes calldata data ) external override returns (bytes4) { return IERC721Receiver.onERC721Received.selector; }参数含义operator是调用转账函数的地址from是转出 NFT 的地址tokenId是被转移的 NFT 编号data是随转账附带的额外数据。函数直接返回IERC721Receiver.onERC721Received.selector即0x150b7a02表示本合约接受 ERC721 资产。四大交易函数合约实现了挂单、撤单、改价、购买四个交易函数构成完整的链上交易闭环。挂单list()卖家创建订单并转移 NFT 到交易所成功后释放List事件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]; // 设置 NFT 持有人和价格 _order.owner msg.sender; _order.price _price; // 将 NFT 转账到合约 _nft.safeTransferFrom(msg.sender, address(this), _tokenId); // 释放 List 事件 emit List(msg.sender, _nftAddr, _tokenId, _price); }三个参数_nftAddr为 NFT 合约地址_tokenId为对应的 NFT 编号_price为挂单价格注意单位是 wei。挂单有两个前置条件一是getApproved(_tokenId)必须返回交易所合约地址即卖家已调用 NFT 合约的approve()完成授权二是价格必须大于 0。订单记录写入后才执行safeTransferFrom将 NFT 从卖家转入交易所托管这一顺序保证了 NFT 必然与订单同时到位。撤单revoke()卖家取消挂单NFT 从交易所退回卖家订单记录被删除释放Revoke事件function revoke(address _nftAddr, uint256 _tokenId) public { Order storage _order nftList[_nftAddr][_tokenId]; // 取得 Order require(_order.owner msg.sender, Not Owner); // 必须由持有人发起 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); }两个参数_nftAddr为 NFT 合约地址_tokenId为对应 NFT 编号。两道校验缺一不可订单的owner必须是调用者只有挂单人能撤自己的单且ownerOf(_tokenId)必须返回交易所合约确保 NFT 确实还在托管中订单有效。执行完safeTransferFrom退回 NFT 后用delete清空订单。改价update()卖家修改已挂单 NFT 的价格释放Update事件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 _nft IERC721(_nftAddr); require(_nft.ownerOf(_tokenId) address(this), Invalid Order); // NFT 在合约中 // 调整 NFT 价格 _order.price _newPrice; // 释放 Update 事件 emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); }三个参数_nftAddr为 NFT 合约地址_tokenId为对应 NFT 编号_newPrice为更新后的价格单位 wei。校验逻辑与revoke()一致——必须是订单持有人发起、NFT 仍在托管中、新价格大于 0满足后直接改写_order.price。购买purchase()买家带 ETH 调用NFT 转给买家、ETH 转给卖家、多余部分退还给买家释放Purchase事件并删除订单function purchase(address _nftAddr, uint256 _tokenId) public payable { Order storage _order nftList[_nftAddr][_tokenId]; // 取得 Order require(_order.price 0, Invalid Price); // NFT 价格大于 0 require(msg.value _order.price, Increase price); // 购买价格大于标价 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); if (msg.value _order.price) { payable(msg.sender).transfer(msg.value - _order.price); } // 释放 Purchase 事件 emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); delete nftList[_nftAddr][_tokenId]; // 删除 order }注意该函数的签名只有两个参数_nftAddr、_tokenId支付金额通过msg.value附带在 Remix 中填写在 VALUE 输入框里这是事实上的第三个输入。函数声明为payable并校验msg.value _order.price。成交顺序很讲究先把 NFT 安全转给买家再把标价 ETH 转给卖家若买家多付了则立即退款最后删除订单。由于 NFT 与 ETH 在同一笔交易内原子完成互换天然避免了付款拿不到货或发货收不到钱的纠纷。与 ERC721 实现的呼应safeTransferFrom会先校验_isApprovedOrOwner见 34_ERC721/ERC721.sol再执行_safeTransfer→_checkOnERC721Received这正是list()挂单前要求getApproved授权、以及交易所必须实现onERC721Received的原因ownerOf读取_owners映射若 tokenId 不存在会require(owner ! address(0))见 34_ERC721/ERC721.sol因此revoke()/update()/purchase()中ownerOf(_tokenId) address(this)的校验能可靠确认 NFT 处于托管状态本讲使用的测试 NFT 合约 34_ERC721/WTFApe.sol 继承自 34_ERC721/ERC721.sol定义了MAX_APES 10000的总量限制mint(address to, uint tokenId)允许铸造编号 09999 的 NFTbaseURI 指向 IPFS。Remix 实操从铸造到成交的完整流程第 1 步部署并铸造 NFT先按 ERC721 教程 部署WTFApeNFT 合约构造函数需要传入name与symbol本例均为WTF然后给自己铸造两枚 NFTtokenId分别为0和1WTFApe总量 10000编号从 0 开始。mint(address to, uint tokenId)的两个参数to是接收 NFT 的地址通常填自己的钱包地址tokenId是本例中的0/1。铸造后可用ownerOf(0)确认自己确实持有该 NFT。第 2 步部署 NFTSwap 合约在 Remix 中打开 38_NFTSwap/NFTSwap.sol部署NFTSwap合约无构造函数参数。部署成功后把合约地址复制下来后续授权与购买都会用到。第 3 步授权 NFTSwap 托管 NFT在WTFApe合约中调用approve(address to, uint tokenId)把tokenId 0授权给NFTSwap合约地址to填 NFTSwap 合约地址tokenId填0。同理把tokenId 1也授权给NFTSwap。这一步是list()中require(_nft.getApproved(_tokenId) address(this), Need Approval)的前置条件——未授权挂单会被直接拒绝。第 4 步挂单上架在NFTSwap合约中调用list(address _nftAddr, uint256 _tokenId, uint256 _price)上架tokenId 0价格设为 1 wei_nftAddr填WTFApe合约地址_tokenId填0_price填1。用同样方法上架tokenId 1。执行后两枚 NFT 从你的地址转入交易所合约。第 5 步查看挂单调用nftList(_nftAddr, _tokenId)查询订单传入WTFApe合约地址与tokenId返回值为Order结构体owner与price。此时应看到owner为你自己的地址、price为 1 wei。第 6 步修改价格调用update(address _nftAddr, uint256 _tokenId, uint256 _newPrice)把tokenId 0的价格改为 77 wei_nftAddr填WTFApe合约地址_tokenId填0_newPrice填77。改价后再调nftList应看到price已更新为 77。第 7 步撤销挂单调用revoke(address _nftAddr, uint256 _tokenId)撤下tokenId 1的挂单_nftAddr填WTFApe合约地址_tokenId填1。执行后 NFT 退回卖家再次查询nftList会看到订单已不存在返回 owner 为 0 地址、price 为 0。注意撤单后如需重新上架必须从第 3 步开始重新授权。第 8 步购买 NFT切换到另一个账户调用purchase(address _nftAddr, uint256 _tokenId)购买tokenId 0同时把 VALUE 设为 77 weimsg.value_nftAddr填WTFApe合约地址_tokenId填0。由于tokenId 1已被撤单当前可买的就是tokenId 0。第 9 步验证所有权变更购买成功后回到WTFApe合约调用ownerOf(0)会发现该 NFT 的所有者已变为买家账户说明链上成交顺利完成。至此一个铸造 → 授权 → 挂单 → 改价 → 撤单 → 购买 → 验证的完整 NFT 交易所闭环就跑通了。从源码结构看设计取舍与安全边界零手续费整个合约没有任何fee字段或平台分成逻辑成交价格全额归卖家这是与中心化平台最本质的区别。原子化结算purchase()在同一次交易内完成 NFT 转移、ETH 结算与找零退款不存在两阶段结算导致的中途失败风险。防重复订单list()对同一tokenId直接覆写订单但由于托管方是唯一、且成交/撤单后订单被delete同一tokenId同时只会存在一个有效挂单。前置授权强约束挂单前必须完成approve授权NFT 实际持有权与订单记录强绑定从代码结构上杜绝了空手套白狼式的虚假挂单。可观测性四个事件全部带indexed字段链下索引与行情展示可直接订阅这也解释了为什么事件设计是这类交易所协议的核心组成部分。需要说明的是作为教学示例本合约聚焦核心交易逻辑未包含法务合规所需的 KYC、版税分成、批量挂单等生产级特性若需深入学习 ERC721 底层的授权与安全转账机制可继续阅读 34_ERC721/readme.md。总结本讲用约 130 行 Solidity 代码构建了一个零手续费的去中心化 NFT 交易所以Order结构体 双层 mapping 实现轻量订单簿以fallback承接 ETH以onERC721Received合规接收 ERC721以list/revoke/update/purchase四个函数闭环整个交易生命周期。整个流程在 Remix 中即可验证代码可直接作为学习 ERC721 生态与链上交易协议设计的起点。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价