目前来看,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;
}
}
/// @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是交易费的接受者
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
constructor(address payable _feeReceiver, uint256 _feeRate) public {
feeReceiver = _feeReceiver;
feeRate = _feeRate;
initReentrancyStatus();
}
构造函数主要是传入手续费和手续费接受者,然后初始化ReentrancyStatus,这个状态是用来防重入的,具体可以看ReentrancyGuard的代码。
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查看交易价格。但是用户无法修改交易信息,如果要修改,就必须先取消交易,然后重新出售。
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转还给卖家。
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.
/// @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:查询交易总量
a、市场管理相关方法有可能导致交易被阻塞,比如通过enableSales关闭市场,通过removeSupportedNft和removeSupportedCurrency阻止交易。这些方法的调用应该通过社区治理的方式去执行
b、交易被取消后,仅仅是改变状态,因此随着时间推移,这部分数据会越来越多。其实,被取消的交易信息完全可以使用event的形式保留,SalesObject的信息可以被删掉。可以考虑使用openzeppelin的EnumerableMap来保存信息。