WTF Solidity极简入门: 38. NFT交易所
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
Opensea
是以太坊上最大的NFT
交易平台,总交易总量达到了$300亿
。Opensea
在交易中抽成2.5%
,因此它通过用户交易至少获利了$7.5亿
。另外,它的运作并不去中心化,且不准备发币补偿用户。NFT
玩家苦Opensea
久已,今天我们就利用智能合约搭建一个零手续费的去中心化NFT
交易所:NFTSwap
。
设计逻辑
- 卖家:出售
NFT
的一方,可以挂单list
、撤单revoke
、修改价格update
。 - 买家:购买
NFT
的一方,可以购买purchase
。 - 订单:卖家发布的
NFT
链上订单,一个系列的同一tokenId
最多存在一个订单,其中包含挂单价格price
和持有人owner
信息。当一个订单交易完成或被撤单后,其中信息清零。
NFTSwap
合约
事件
合约包含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;
回退函数
在NFTSwap
中,用户使用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, _order.price);
}
Remix
实现
1. 部署NFT合约
参考 ERC721 教程了解NFT,并部署WTFApe
NFT合约。
将首个NFT mint给自己,这里mint给自己是为了之后能够上架NFT、修改价格等一系类操作。
mint(address to, uint tokenId)
方法有2个参数:
to
:将 NFT mint给指定的地址,这里通常是自己的钱包地址。
tokenId
: WTFApe
合约定义了总量为10000个NFT,图中mint它的的第一个和第二个NFT,tokenId
分别为0
和1
。
在WTFApe
合约中,利用ownerOf
确认自己已经获得tokenId
为0的NFT。
ownerOf(uint tokenId)
方法有1个参数:
tokenId
: tokenId
为NFT的id,本案例中为上述mint的0
Id。
按照上述方法,将TokenId为 0
和 1
的NFT都mint给自己,其中tokenId
为0
的,我们执行更新购买操作,tokenId
为1
的,我们执行下架操作。
2. 部署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的0
Id。
按照上述方法,同理将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的0
Id。
_price
: _price
为NFT的价格,本案例中为1 wei
。
按照上述方法,同理将自己持有的tokenId
为1的NFT上架到NFTSwap
,价格设为1 wei
。
5. 查看上架NFT
调用NFTSwap
合约的nftList()
函数查看上架的NFT。
nftList
:是一个NFT Order的映射,结构如下:
nftList[_nftAddr][_tokenId]
: 输入_nftAddr
和_tokenId
,返回一个NFT订单。
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的0
Id。
_newPrice
: _newPrice
为NFT的新价格,本案例中为77 wei
。
执行update
之后,调用nftList
查看更新后的价格
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的1
Id。
调用NFTSwap
合约的nftList()
函数,可以看到NFT
已经下架。再次上架需要重新授权。
注意下架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的0
Id。
_wei
: _wei
为支付的ETH
数量,本案例中为77 wei
。
7. 验证NFT
持有人改变
购买成功之后,调用WTFApe
合约的ownerOf()
函数,可以看到NFT
持有者发生变化,购买成功!
总结
这一讲,我们建立了一个零手续费的去中心化NFT
交易所。OpenSea
虽然对NFT
的发展做了很大贡献,但它的缺点也非常明显:高手续费、不发币回馈用户、交易机制容易被钓鱼导致用户资产丢失。目前Looksrare
和dydx
等新的NFT
交易平台正在挑战OpenSea
的位置,Uniswap
也在研究新的NFT
交易所。相信不久的将来,我们会用到更好的NFT
交易所。