原文发布在 https://github.com/33357/smartcontract-apps这是一个面向中文社区,分析市面上智能合约应用的架构与实现的仓库。欢迎关注开源知识项目!
2022年2月15日,X2Y2 正式开启空投、NFT挂单奖励和质押挖矿,随后的十几天内其币价一泻千里。最低到0.25 u/X2Y2,相比最高点跌了90%以上。直到官方宣布修改NFT挂单奖励,大幅减少产出,币价才重新稳定在0.35 u/X2Y2左右。
X2Y2 币价的下跌是多重因素的共同作用,其相比 OpenSea 新创的交易手续费返还 X2Y2 持有者和 NFT 挂单奖励是具有相当程度先进性的。但是我认为:中心化、不透明、无法预测的 NFT 挂单奖励机制在区块链乃至整个 Web3 世界都是不可持续的,主要问题有以下几点:
NFT挂单奖励算法会被随意修改,用户无法做长期决策(之前团队为了稳定币价修改算法的决策值得肯定,但严重削弱了挂单用户对项目的信任)。
用户无法对挂单奖励进行预测,因此无法对是否挂单做理性决策(具体来说就是无法在挂单之前对挂单奖励做预测,这会导致很多人要么抱着赌一把的心态来挂单,要么抱着不信任的态度走人)。
团队有持币跑路的风险(区块链世界里代码才是法律,甚至高于法律,达到了物理规则的高度。团队跑路的成本和收益相比不值一提,因此永远不要高估人性)。
之前我在项目方群里讨论这个问题时,有很多人认为 NFT 挂单奖励对 NFT 挂单者是一种 “恩惠”,而不是一种商业模式:你在 OpenSea 挂单啥都没有,来这里还有可能获得奖励,因此中心化的 NFT 挂单奖励机制的这些弊病是没有问题的。我觉得如果真是这样,那么这个项目就不足以对 OpenSea 在商业模式上进行创新,而大多数用户没有足够多确定的利益是懒得去换平台的。
这是一个区块链技术方向的社群,因此讨论不会仅涉及于提出问题而没有解决方案。在去中心化的代币奖励方面,早有前人为我们指明了一条道路,那就是目前普遍用于代币质押的挖矿奖励算法(篇幅所限,我就不去解释这个算法了)。我们只需要知道,这个算法实现了随时间和质押代币数量成正比的奖励。在个条件下,时间是不需要改变的,而 NFT 不是代币。因此只要实现从 NFT 到代币的转换,就完全可以实现去中心化的 NFT 挂单奖励。
这是一个简单的算法,使用挂单价格作为唯一参数,实现从 NFT 到代币的转换
// 最大价格
uint256 constant MAX_PRICE = 10**26;
// 最小价格
uint256 constant MIN_PRICE = 10**10;
// 预估分数
function predictPoint(uint256 price) public pure override returns (uint256) {
// 价格不能超过范围
require(price <= MAX_PRICE && price >= MIN_PRICE, "NFT: price is too high or too low");
// 计算分数
return (MAX_PRICE * MIN_PRICE) / price;
}
在这里,我把从 NFT 到代币的转换过程抽象为给每个 NFT 打分的过程。NFT 作为非同质化代币,想要准确地给每个 NFT 打分,只有从挂单价格入手:价格越高的 NFT 其分数越低。
在这里我们可以演算一下:如果一共有10个 NFT,其价格分别是 1、2、3、4、5、6、7、8、9、10 ETH,那么打分的情况会是什么?
(10**26 * 10**10) / 10**18 = 10**18
(10**26 * 10**10) / (2 * 10**18) = 10**18 / 2
(10**26 * 10**10) / (3 * 10**18) = 10**18 / 3
(10**26 * 10**10) / (4 * 10**18) = 10**18 / 4
(10**26 * 10**10) / (5 * 10**18) = 10**18 / 5
(10**26 * 10**10) / (6 * 10**18) = 10**18 / 6
(10**26 * 10**10) / (7 * 10**18) = 10**18 / 7
(10**26 * 10**10) / (8 * 10**18) = 10**18 / 8
(10**26 * 10**10) / (9 * 10**18) = 10**18 / 9
(10**26 * 10**10) / (10 * 10**18) = 10**18 / 10
point 总数为 10**18 * (7381/2520)
,每个 NFT 代币可以分到奖励的份额如下:
2520/7381
1260/7381
840/7381
630/7381
504/7381
420/7381
360/7381
315/7381
280/7381
252/7381
可以看到,随着 NFT 挂单价格的升高,其分数会下降,奖励份额也会下降。
一般而言NFT的挂单价格不会如此平均,一般会呈现中间多,两头少的“橄榄球”形状,这意味着挂单价格低的用户会拥有比这个演算更多的奖励。
对于不同类型的NFT,需要给予不同的挂单奖励。
由于不同类型的 NFT 之间价格差异巨大,因此不可能对所有种类的 NFT 给予相同的奖励池。实际上每种 NFT 的奖励池额度需要单独计算,对此又可以衍生出好几种不同的算法:
使用中心化的算法
实际上目前 X2Y2 就是使用的这种算法,其效果只能说是差强人意。
使用去中心化的算法
可以使用每种 NFT 的交易手续费作为参数,对奖励额度在链上进行动态调整。我预计是可行的,但是在没有手续费时,项目如何启动是一个问题。
使用 DAO 进行管理
对上述的方案进行中和,使用 DAO 让社区成员投票决定给某种 NFT 多少奖励池。这种方案依赖良好的社群用户。
对挂单奖励进行预测
经典的挖矿奖励算法已经实现了对奖励的预测,还可以计算 APY。由于 NFT 难确定标准的市场价格,因此我估计只能在用户输入挂单价格后,预测每天获得多少奖励。
对挂买单进行奖励
实际上对上述的挂单 NFT 奖励算法反过来,就可以对挂 NFT 买单进行奖励。也许可以借此做出一个 NFT 的公允市场价。
目前的问题
这套方案里挂单需要上链,改价格也需要上链,因此和 OpenSea 目前的模式并不完全兼容。
挂单需要将 NFT 的所有权转移到合约,因此不能同时挂在 OpenSea 上。
X2Y2 在 OpenSea 的基础上进行了创新,却无法给 OpenSea 用户足够的利益让他们改换门庭。但无论如何,OpenSea 再不改革把自己的利益分享出来,下个四年就看不到它的身影了。
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.12;
import "./interfaces/INFT.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract NFT is INFT, ReentrancyGuard {
using SafeERC20 for IERC20;
mapping(address => uint256) blockMint;
mapping(address => uint256) totalPoint;
mapping(address => uint256) perPointMinted;
mapping(address => uint256) lastUpdateBlock;
mapping(address => mapping(uint256 => uint256)) tokenPoint;
mapping(address => mapping(uint256 => address)) tokenOwner;
mapping(address => mapping(uint256 => uint256)) tokenPrice;
mapping(address => mapping(uint256 => uint256)) tokenMinted;
mapping(address => mapping(uint256 => uint256)) tokenPerPointPaid;
uint256 constant MAX_PRICE = 10**26;
uint256 constant MIN_PRICE = 10**10;
uint256 fee = 200;
address feeTo;
constructor() {}
/* ================ UTIL FUNCTIONS ================ */
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, "NFT: ETH transfer failed");
}
function perPointMint(address nft) internal view returns (uint256) {
if (totalPoint[nft] != 0) {
return perPointMinted[nft] + ((block.number - lastUpdateBlock[nft]) * blockMint[nft]) / totalPoint[nft];
} else {
return perPointMinted[nft];
}
}
modifier _updateMint(address nft, uint256 tokenId) {
if (block.number > lastUpdateBlock[nft]) {
perPointMinted[nft] = perPointMint(nft);
lastUpdateBlock[nft] = block.number;
}
if (totalPoint[nft] != 0) {
tokenMinted[nft][tokenId] = tokenMint(nft, tokenId);
}
tokenPerPointPaid[nft][tokenId] = perPointMinted[nft];
_;
}
/* ================ VIEW FUNCTIONS ================ */
function predictPoint(uint256 price) public pure override returns (uint256) {
require(price <= MAX_PRICE && price >= MIN_PRICE, "NFT: price is too high or too low");
return (MAX_PRICE * MIN_PRICE) / price;
}
function tokenMint(address nft, uint256 tokenId) public view override returns (uint256) {
return
tokenMinted[nft][tokenId] +
(tokenPoint[nft][tokenId] * (perPointMint(nft) - tokenPerPointPaid[nft][tokenId]));
}
/* ================ TRANSACTION FUNCTIONS ================ */
function list(
address nft,
uint256 tokenId,
uint256 price
) external override nonReentrant {
tokenPoint[nft][tokenId] = predictPoint(price);
totalPoint[nft] += tokenPoint[nft][tokenId];
tokenPrice[nft][tokenId] = price;
tokenOwner[nft][tokenId] = msg.sender;
IERC721(nft).safeTransferFrom(msg.sender, address(this), tokenId);
}
function unList(address nft, uint256 tokenId) external override nonReentrant {
require(tokenOwner[nft][tokenId] == msg.sender, "NFT: sender not owner");
require(IERC721(nft).ownerOf(tokenId) == address(this), "NFT: this not owner");
totalPoint[nft] -= tokenPoint[nft][tokenId];
tokenPoint[nft][tokenId] = 0;
tokenOwner[nft][tokenId] = address(0);
tokenPrice[nft][tokenId] = 0;
IERC721(nft).safeTransferFrom(address(this), msg.sender, tokenId);
}
function rePrice(
address nft,
uint256 tokenId,
uint256 price
) external override nonReentrant {
require(tokenOwner[nft][tokenId] == msg.sender, "NFT: sender not owner");
require(IERC721(nft).ownerOf(tokenId) == address(this), "NFT: this not owner");
totalPoint[nft] -= tokenPoint[nft][tokenId];
tokenPoint[nft][tokenId] = predictPoint(price);
totalPoint[nft] += tokenPoint[nft][tokenId];
tokenPrice[nft][tokenId] = price;
}
function buy(address nft, uint256 tokenId) external payable override nonReentrant {
require(msg.value >= tokenPrice[nft][tokenId], "NFT: price is too low");
require(IERC721(nft).ownerOf(tokenId) == address(this), "NFT: this not owner");
uint256 feeAmount = (tokenPrice[nft][tokenId] * fee) / 10000;
totalPoint[nft] -= tokenPoint[nft][tokenId];
tokenPoint[nft][tokenId] = 0;
tokenOwner[nft][tokenId] = address(0);
safeTransferETH(feeTo, feeAmount);
safeTransferETH(tokenOwner[nft][tokenId], tokenPrice[nft][tokenId] - feeAmount);
tokenPrice[nft][tokenId] = 0;
IERC721(nft).safeTransferFrom(address(this), msg.sender, tokenId);
if (address(this).balance > 0) {
safeTransferETH(msg.sender, address(this).balance);
}
}
/* ================ ADMIN FUNCTIONS ================ */
function setBlockMint(address nft, uint256 newBlockMint) external {
blockMint[nft] = newBlockMint;
}
function setFee(uint256 newFee) external {
fee = newFee;
}
function setFeeTo(address newFeeTo) external {
feeTo = newFeeTo;
}
}