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,那么打分的情况会是什么?
- point1:
(10**26 * 10**10) / 10**18 = 10**18
- point2:
(10**26 * 10**10) / (2 * 10**18) = 10**18 / 2
- point3:
(10**26 * 10**10) / (3 * 10**18) = 10**18 / 3
- point4:
(10**26 * 10**10) / (4 * 10**18) = 10**18 / 4
- point5:
(10**26 * 10**10) / (5 * 10**18) = 10**18 / 5
- point6:
(10**26 * 10**10) / (6 * 10**18) = 10**18 / 6
- point7:
(10**26 * 10**10) / (7 * 10**18) = 10**18 / 7
- point8:
(10**26 * 10**10) / (8 * 10**18) = 10**18 / 8
- point9:
(10**26 * 10**10) / (9 * 10**18) = 10**18 / 9
- point10:
(10**26 * 10**10) / (10 * 10**18) = 10**18 / 10
point 总数为
10**18 * (7381/2520)
,每个 NFT 代币可以分到奖励的份额如下:- token1:
2520/7381
- token2:
1260/7381
- token3:
840/7381
- token4:
630/7381
- token5:
504/7381
- token6:
420/7381
- token7:
360/7381
- token8:
315/7381
- token9:
280/7381
- token10:
252/7381
可以看到,随着 NFT 挂单价格的升高,其分数会下降,奖励份额也会下降。
一般而言NFT的挂单价格不会如此平均,一般会呈现中间多,两头少的“橄榄球”形状,这意味着挂单价格低的用户会拥有比这个演算更多的奖励。
- point1:
-
-
对于不同类型的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; } }