当前位置:网站首页>NFT Market的一种实现
NFT Market的一种实现
2022-07-21 05:08:00 【biakia0610】
目前来看,seascape实现的NftMarket可能是功能比较全面的NFT市场合约。
代码来源:seascape-smartcontracts/NftMarket.sol at main · blocklords/seascape-smartcontracts · GitHub
pragma solidity ^0.6.7;
pragma experimental ABIEncoderV2;
import "./../openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./../openzeppelin/contracts/math/SafeMath.sol";
import "./../openzeppelin/contracts/token/ERC721/IERC721.sol";
import "./../openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "./../openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "./../openzeppelin/contracts/access/Ownable.sol";
import "./../seascape_nft/SeascapeNft.sol";
import "./ReentrancyGuard.sol";
/// @title Nft Market is a trading platform on seascape network allowing to buy and sell Nfts
/// @author Nejc Schneider
contract NftMarket is IERC721Receiver, ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
using SafeMath for uint256;
/// @notice individual sale related data
struct SalesObject {
uint256 id; // sales ID
uint256 tokenId; // token unique id
address nft; // nft address
address currency; // currency address
address payable seller; // seller address
address payable buyer; // buyer address
uint256 startTime; // timestamp when the sale starts
uint256 price; // nft price
uint8 status; // 2 = sale canceled, 1 = sold, 0 = for sale
}
/// @dev keep count of SalesObject amount
uint256 public salesAmount;
/// @dev store sales objects.
/// @param nft token address => (nft id => salesObject)
mapping(address => mapping(uint256 => SalesObject)) salesObjects; // store sales in a mapping
/// @dev supported ERC721 and ERC20 contracts
mapping(address => bool) public supportedNft;
mapping(address => bool) public supportedCurrency;
/// @notice enable/disable trading
bool public salesEnabled;
/// @dev fee rate and fee reciever. feeAmount = (feeRate / 1000) * price
uint256 public feeRate;
address payable feeReceiver;
event Buy(
uint256 indexed id,
uint256 tokenId,
address buyer,
uint256 price,
uint256 tipsFee,
address currency
);
event Sell(
uint256 indexed id,
uint256 tokenId,
address nft,
address currency,
address seller,
address buyer,
uint256 startTime,
uint256 price
);
event SaleCanceled(uint256 indexed id, uint256 tokenId);
event NftReceived(address operator, address from, uint256 tokenId, bytes data);
/// @dev set fee reciever address and fee rate
/// @param _feeReceiver fee receiving address
/// @param _feeRate fee amount
constructor(address payable _feeReceiver, uint256 _feeRate) public {
feeReceiver = _feeReceiver;
feeRate = _feeRate;
initReentrancyStatus();
}
//--------------------------------------------------
// External methods
//--------------------------------------------------
/// @notice enable/disable sales
/// @param _salesEnabled set sales to true/false
function enableSales(bool _salesEnabled) external onlyOwner { salesEnabled = _salesEnabled; }
/// @notice add supported nft token
/// @param _nftAddress ERC721 contract address
function addSupportedNft(address _nftAddress) external onlyOwner {
require(_nftAddress != address(0x0), "invalid address");
supportedNft[_nftAddress] = true;
}
/// @notice disable supported nft token
/// @param _nftAddress ERC721 contract address
function removeSupportedNft(address _nftAddress) external onlyOwner {
require(_nftAddress != address(0x0), "invalid address");
supportedNft[_nftAddress] = false;
}
/// @notice add supported currency token
/// @param _currencyAddress ERC20 contract address
function addSupportedCurrency(address _currencyAddress) external onlyOwner {
require(!supportedCurrency[_currencyAddress], "currency already supported");
supportedCurrency[_currencyAddress] = true;
}
/// @notice disable supported currency token
/// @param _currencyAddress ERC20 contract address
function removeSupportedCurrency(address _currencyAddress) external onlyOwner {
require(supportedCurrency[_currencyAddress], "currency already removed");
supportedCurrency[_currencyAddress] = false;
}
/// @notice change fee receiver address
/// @param _walletAddress address of the new fee receiver
function setFeeReceiver(address payable _walletAddress) external onlyOwner {
require(_walletAddress != address(0x0), "invalid address");
feeReceiver = _walletAddress;
}
/// @notice change fee rate
/// @param _rate amount value. Actual rate in percent = _rate / 10
function setFeeRate(uint256 _rate) external onlyOwner {
require(_rate <= 100, "Rate should be bellow 100 (10%)");
feeRate = _rate;
}
/// @notice returns sales amount
/// @return total amount of sales objects
function getSalesAmount() external view returns(uint) { return salesAmount; }
//--------------------------------------------------
// Public methods
//--------------------------------------------------
/// @notice cancel nft sale
/// @param _tokenId nft unique ID
/// @param _nftAddress nft token address
function cancelSell(uint _tokenId, address _nftAddress) public nonReentrant {
SalesObject storage obj = salesObjects[_nftAddress][_tokenId];
require(obj.status == 0, "status: sold or canceled");
require(obj.seller == msg.sender, "seller not nft owner");
require(salesEnabled, "sales are closed");
obj.status = 2;
IERC721 nft = IERC721(obj.nft);
nft.safeTransferFrom(address(this), obj.seller, obj.tokenId);
emit SaleCanceled(_tokenId, obj.tokenId);
}
/// @notice put nft for sale
/// @param _tokenId nft unique ID
/// @param _price required price to pay by buyer. Seller receives less: price - fees
/// @param _nftAddress nft token address
/// @param _currency currency token address
/// @return salesAmount total amount of sales
function sell(uint256 _tokenId, uint256 _price, address _nftAddress, address _currency)
public
nonReentrant
returns(uint)
{
require(_nftAddress != address(0x0), "invalid nft address");
require(_tokenId != 0, "invalid nft token");
require(salesEnabled, "sales are closed");
require(supportedNft[_nftAddress], "nft address unsupported");
require(supportedCurrency[_currency], "currency not supported");
IERC721(_nftAddress).safeTransferFrom(msg.sender, address(this), _tokenId);
salesAmount++;
salesObjects[_nftAddress][_tokenId] = SalesObject(
salesAmount,
_tokenId,
_nftAddress,
_currency,
msg.sender,
address(0x0),
now,
_price,
0
);
emit Sell(
salesAmount,
_tokenId,
_nftAddress,
_currency,
msg.sender,
address(0x0),
now,
_price
);
return salesAmount;
}
/// @dev encrypt token data
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes memory data
)
public
override
returns (bytes4)
{
//only receive the _nft staff
if (address(this) != operator) {
//invalid from nft
return 0;
}
//success
emit NftReceived(operator, from, tokenId, data);
return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
}
/// @notice buy nft
/// @param _tokenId nft unique ID
/// @param _nftAddress nft token address
/// @param _currency currency token address
function buy(uint _tokenId, address _nftAddress, address _currency)
public
nonReentrant
payable
{
SalesObject storage obj = salesObjects[_nftAddress][_tokenId];
require(obj.status == 0, "status: sold or canceled");
require(obj.startTime <= now, "not yet for sale");
require(salesEnabled, "sales are closed");
require(msg.sender != obj.seller, "cant buy from yourself");
require(obj.currency == _currency, "must pay same currency as sold");
uint256 price = this.getSalesPrice(_tokenId, _nftAddress);
uint256 tipsFee = price.mul(feeRate).div(1000);
uint256 purchase = price.sub(tipsFee);
if (obj.currency == address(0x0)) {
require (msg.value >= price, "your price is too low");
uint256 returnBack = msg.value.sub(price);
if (returnBack > 0)
msg.sender.transfer(returnBack);
if (tipsFee > 0)
feeReceiver.transfer(tipsFee);
obj.seller.transfer(purchase);
} else {
IERC20(obj.currency).safeTransferFrom(msg.sender, feeReceiver, tipsFee);
IERC20(obj.currency).safeTransferFrom(msg.sender, obj.seller, purchase);
}
IERC721 nft = IERC721(obj.nft);
nft.safeTransferFrom(address(this), msg.sender, obj.tokenId);
obj.buyer = msg.sender;
obj.status = 1;
emit Buy(obj.id, obj.tokenId, msg.sender, price, tipsFee, obj.currency);
}
/// @dev fetch sale object at nftId and nftAddress
/// @param _tokenId unique nft ID
/// @param _nftAddress nft token address
/// @return SalesObject at given index
function getSales(uint _tokenId, address _nftAddress)
public
view
returns(SalesObject memory)
{
return salesObjects[_nftAddress][_tokenId];
}
/// @dev returns the price of sale
/// @param _tokenId nft unique ID
/// @param _nftAddress nft token address
/// @return obj.price price of corresponding sale
function getSalesPrice(uint _tokenId, address _nftAddress) public view returns (uint256) {
SalesObject storage obj = salesObjects[_nftAddress][_tokenId];
return obj.price;
}
}
1、数据结构
/// @notice individual sale related data
struct SalesObject {
uint256 id; // sales ID
uint256 tokenId; // token unique id
address nft; // nft address
address currency; // currency address
address payable seller; // seller address
address payable buyer; // buyer address
uint256 startTime; // timestamp when the sale starts
uint256 price; // nft price
uint8 status; // 2 = sale canceled, 1 = sold, 0 = for sale
}
/// @dev keep count of SalesObject amount
uint256 public salesAmount;
/// @dev store sales objects.
/// @param nft token address => (nft id => salesObject)
mapping(address => mapping(uint256 => SalesObject)) salesObjects; // store sales in a mapping
/// @dev supported ERC721 and ERC20 contracts
mapping(address => bool) public supportedNft;
mapping(address => bool) public supportedCurrency;
/// @notice enable/disable trading
bool public salesEnabled;
/// @dev fee rate and fee reciever. feeAmount = (feeRate / 1000) * price
uint256 public feeRate;
address payable feeReceiver;
SalesObject是交易主体的数据结构,保存了一系列交易信息。其中id类似于数据库里的主键,tokenId是NFT的ID,nft是NFT对应的智能合约,currency是购买该NFT的币种合约,seller和buyer是卖家和买家地址,startTime是售卖开始时间,price是以currency计价的金额,status是交易状态,0表示出售中,1表示已出售,2表示已取消
salesAmount是个全局变量,表示SalesObject生成的数量,对应SalesObject里的id
salesObjects是个索引,可以通过NFT合约地址以及NFT的ID找到对应的交易信息SalesObject
supportedNft是NFT合约的白名单
supportedCurrency是购买币种的白名单
salesEnabled是个全局开关,关了一切交易都会停止
feeRate是每一笔交易的费用
feeReceiver是交易费的接受者
2、onERC721Received
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes memory data
)
public
override
returns (bytes4)
{
//only receive the _nft staff
if (address(this) != operator) {
//invalid from nft
return 0;
}
//success
emit NftReceived(operator, from, tokenId, data);
return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
}
这个是实现了ERC721的标准,表示该合约可以接收NFT,具体可以看:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
3、构造函数
constructor(address payable _feeReceiver, uint256 _feeRate) public {
feeReceiver = _feeReceiver;
feeRate = _feeRate;
initReentrancyStatus();
}
构造函数主要是传入手续费和手续费接受者,然后初始化ReentrancyStatus,这个状态是用来防重入的,具体可以看ReentrancyGuard的代码。
4、卖家挂单出售NFT
function sell(uint256 _tokenId, uint256 _price, address _nftAddress, address _currency)
public
nonReentrant
returns(uint)
{
require(_nftAddress != address(0x0), "invalid nft address");
require(_tokenId != 0, "invalid nft token");
require(salesEnabled, "sales are closed");
require(supportedNft[_nftAddress], "nft address unsupported");
require(supportedCurrency[_currency], "currency not supported");
IERC721(_nftAddress).safeTransferFrom(msg.sender, address(this), _tokenId);
salesAmount++;
salesObjects[_nftAddress][_tokenId] = SalesObject(
salesAmount,
_tokenId,
_nftAddress,
_currency,
msg.sender,
address(0x0),
now,
_price,
0
);
emit Sell(
salesAmount,
_tokenId,
_nftAddress,
_currency,
msg.sender,
address(0x0),
now,
_price
);
return salesAmount;
}
function getSales(uint _tokenId, address _nftAddress)
public
view
returns(SalesObject memory)
{
return salesObjects[_nftAddress][_tokenId];
}
function getSalesPrice(uint _tokenId, address _nftAddress) public view returns (uint256) {
SalesObject storage obj = salesObjects[_nftAddress][_tokenId];
return obj.price;
}
这里注意,一定是先有用户挂单出售,才会有用户来购买,所以sell肯定是第一个调用。
这个方法首先校验NFT的存在性,以及白名单,然后把NFT转入到当前合约,然后生成一个SalesObject。卖家可以通过getSales查看交易信息,也可以通过getSalesPrice查看交易价格。但是用户无法修改交易信息,如果要修改,就必须先取消交易,然后重新出售。
5、卖家取消交易
function cancelSell(uint _tokenId, address _nftAddress) public nonReentrant {
SalesObject storage obj = salesObjects[_nftAddress][_tokenId];
require(obj.status == 0, "status: sold or canceled");
require(obj.seller == msg.sender, "seller not nft owner");
require(salesEnabled, "sales are closed");
obj.status = 2;
IERC721 nft = IERC721(obj.nft);
nft.safeTransferFrom(address(this), obj.seller, obj.tokenId);
emit SaleCanceled(_tokenId, obj.tokenId);
}
这个方法用来取消交易,先会校验交易状态是否为0,然后判断权限,只有seller才能取消,还会看下salesEnabled是否关闭,最后把交易状态改成2,并且把当前合约持有的NFT转还给卖家。
6、买家购买NFT
function buy(uint _tokenId, address _nftAddress, address _currency)
public
nonReentrant
payable
{
SalesObject storage obj = salesObjects[_nftAddress][_tokenId];
require(obj.status == 0, "status: sold or canceled");
require(obj.startTime <= now, "not yet for sale");
require(salesEnabled, "sales are closed");
require(msg.sender != obj.seller, "cant buy from yourself");
require(obj.currency == _currency, "must pay same currency as sold");
uint256 price = this.getSalesPrice(_tokenId, _nftAddress);
uint256 tipsFee = price.mul(feeRate).div(1000);
uint256 purchase = price.sub(tipsFee);
if (obj.currency == address(0x0)) {
require (msg.value >= price, "your price is too low");
uint256 returnBack = msg.value.sub(price);
if (returnBack > 0)
msg.sender.transfer(returnBack);
if (tipsFee > 0)
feeReceiver.transfer(tipsFee);
obj.seller.transfer(purchase);
} else {
IERC20(obj.currency).safeTransferFrom(msg.sender, feeReceiver, tipsFee);
IERC20(obj.currency).safeTransferFrom(msg.sender, obj.seller, purchase);
}
IERC721 nft = IERC721(obj.nft);
nft.safeTransferFrom(address(this), msg.sender, obj.tokenId);
obj.buyer = msg.sender;
obj.status = 1;
emit Buy(obj.id, obj.tokenId, msg.sender, price, tipsFee, obj.currency);
}
购买流程首先会校验交易的状态,只有状态为0的交易才是合法的,然后交易是否已经到达开卖时间以及交易市场是否开启,同时会剔除自己买自己的情况,最后校验一下付款币种是否一致。
这里付款币种有两种情况,一种是用ETH付款,还有一种是用ERC20付款。
如果是ETH付款,那么obj.currency就是0地址,用户在执行交易的时候要传入ETH付款金额,对应的就是msg.value这个值。msg.value必须大于price,price一部分发送给手续费接受者,一部分直接发给obj.seller。然后把多余的msg.value返回给买家。如果不返回,这笔钱就会一直留在这个合约中,由于合约没有方法提币,所以会导致这笔钱永远无法被使用。
如果是ERC20付款,就直接使用safeTransferFrom方法给手续费接受者和obj.seller付款。
最后将NFT从当前合约转移到买家,然后设置交易信息中的买家地址,最后设置交易状态为1.
7、市场管理
/// @notice enable/disable sales
/// @param _salesEnabled set sales to true/false
function enableSales(bool _salesEnabled) external onlyOwner { salesEnabled = _salesEnabled; }
/// @notice add supported nft token
/// @param _nftAddress ERC721 contract address
function addSupportedNft(address _nftAddress) external onlyOwner {
require(_nftAddress != address(0x0), "invalid address");
supportedNft[_nftAddress] = true;
}
/// @notice disable supported nft token
/// @param _nftAddress ERC721 contract address
function removeSupportedNft(address _nftAddress) external onlyOwner {
require(_nftAddress != address(0x0), "invalid address");
supportedNft[_nftAddress] = false;
}
/// @notice add supported currency token
/// @param _currencyAddress ERC20 contract address
function addSupportedCurrency(address _currencyAddress) external onlyOwner {
require(!supportedCurrency[_currencyAddress], "currency already supported");
supportedCurrency[_currencyAddress] = true;
}
/// @notice disable supported currency token
/// @param _currencyAddress ERC20 contract address
function removeSupportedCurrency(address _currencyAddress) external onlyOwner {
require(supportedCurrency[_currencyAddress], "currency already removed");
supportedCurrency[_currencyAddress] = false;
}
/// @notice change fee receiver address
/// @param _walletAddress address of the new fee receiver
function setFeeReceiver(address payable _walletAddress) external onlyOwner {
require(_walletAddress != address(0x0), "invalid address");
feeReceiver = _walletAddress;
}
/// @notice change fee rate
/// @param _rate amount value. Actual rate in percent = _rate / 10
function setFeeRate(uint256 _rate) external onlyOwner {
require(_rate <= 100, "Rate should be bellow 100 (10%)");
feeRate = _rate;
}
/// @notice returns sales amount
/// @return total amount of sales objects
function getSalesAmount() external view returns(uint) { return salesAmount; }
enableSales : 用来开关市场
addSupportedNft:添加NFT白名单
removeSupportedNft:移除NFT白名单
addSupportedCurrency:添加付款币种白名单
removeSupportedCurrency:移除付款币种白名单
setFeeReceiver:设置手续费接受地址
setFeeRate:设置手续费费率
getSalesAmount:查询交易总量
8、风险点
a、市场管理相关方法有可能导致交易被阻塞,比如通过enableSales关闭市场,通过removeSupportedNft和removeSupportedCurrency阻止交易。这些方法的调用应该通过社区治理的方式去执行
b、交易被取消后,仅仅是改变状态,因此随着时间推移,这部分数据会越来越多。其实,被取消的交易信息完全可以使用event的形式保留,SalesObject的信息可以被删掉。可以考虑使用openzeppelin的EnumerableMap来保存信息。
边栏推荐
猜你喜欢
【内网渗透】cobaltstrike流量加密
[vulnerability recurrence] redis unauthorized access to windows+linux utilization method
【内网渗透】msf反弹流量加密会话
DWVA[SQL-Injection]学习记录
Thinkphp6 learning experience
Thinkphp6 uses easywechat5 Development of official account of X (I)
[reverse analysis] simple learning of C language pointer
Rust简短笔记:Cargo指定依赖版本
攻防世界web区 难度等级:3(ics-05,MFW,easytornado)
[intranet penetration] intranet penetration of vulnstack II
随机推荐
Planned tasks under laravel5.1
NVM, NRM tutorial
PHP intercepts the contents before and after the specified string
PHP arrays are arranged in descending order according to the keys of the associative array
【漏洞复现】redis未授权访问windows+linux利用方法
[intranet penetration] cobaltstrike traffic encryption
Win9x在Ryzen等新处理器虚拟机抽风的原因及补丁
BUUCTF [SUCTF 2019]EasySQL
Solana项目学习(二): Escrow
求助大神
[permission promotion] MSSQL authorization raising method
火山引擎&搜款网:服装批发背后的智慧与“荐”行
PHP 处理csv 文件 解决中文乱码
Attack and defense World Web Zone difficulty level: 2 (upload1, web2, web_php_include, supersqli, warmup)
Web security -- file upload middleware parsing vulnerability
The text file is transferred to the external server through the web proxy server and returned after modification
【文件上传绕过】--二次渲染
Invalid mouse disabled style (cursor: not allowed) conflicts with mouse disabled events (pointer events: none)
Solana SPL Token 的一些最佳实践
【内网渗透】cobaltstrike流量加密