NFT及OpenSea交易背后的技术分享

这段时间NFT市场火爆,本来准备与大家分享的去中心化存储项目改为了这一篇关于NFT及OpenSea的学习分享了。

最近购入了NFT的各位可以看看你花大价钱购买的NFT的背后究竟是个什么东西,OpenSea是如何实现对NFT进行买卖交易的,这篇文章可以作为一个入门了解。也许这篇文章还是你成为币圈所谓“科学家”的启蒙教程。

从本人第一次购买NFT的故事讲起

鄙人在今年1月3日的时候,从推特上看见有消息说周杰伦发行了他的NFT PhantaBear,所以本着粉丝的心态去了解一下。当时发现小熊的地板价只有0.4ETH,所以立即花了2ETH购入4只小熊。(后来被 @BTCdayu 传授经验应该买稀有属性的,所以之后又购入了两只稀有小熊)

之后两天鄙人在推特上看见很多人说PhantaBear因为周董发表申明切割,所以项目不值得投资,故本人在推特上发表了自己的观点力挺PhantaBear,同时公开打赌PhantaBear一年之后如果年化收益不到300%的话(2E,打赌时小熊地板价在0.6E多点),愿意赠送一只小熊。结果没想到才过了两三天我自己打了自己的脸,关于我的观点与公开赌注大家可以参考这条推特。https://twitter.com/nelsonie/status/1478618887537233922

总的来说就是在用户心智中能够占据第一的项目,都是比较好的标的。比特币是整个区块链行业的开创者,所以是最值得投资的标的;以太坊占据了拥有智能合约功能的第二代区块链中的第一名,所以也是值得投资的(虽然我也没有持仓);CryptoPunks是第一个NFT项目,所以也很值得投资。(18年的时候有个加密猫游戏,我印象中那个好像是NFT始祖,但貌似现在没人提了?)

而PhantaBear是华人圈的第一个NFT项目,同时又是伴随我成长的天王带货的,所以无论是理性上还是感性上都必须拿下。

很多时候自己在crypto行业中的投资,更看中的是这个项目背后的故事及其意义,而一些utility token才会去分析是否有用,是否解决了行业的痛点或者问题。

自购入了人生中的第一个NFT之后,上周整周时间都在刷推特、混小熊的discord(修炼升级、偶尔会有明星聊天)、刷floor price,把原本安排写作的时间都浪费掉了。

所以这周开始摒弃各种刷social network,学习研究OpenSea平台上交易NFT的技术细节并记录分享,毕竟热闹繁华过后还是需要积累沉淀一些东西。

这篇文章鄙人会先讲一讲NFT的合约(ERC721)以及NFT在OpenSea上是如何实现交易的,这两部分的内容比较偏技术,希望可以耐心看完。

前面废话太多,下面正式开始。

NFT与ERC721

NFT的名词定义就不说了,网上有很多资料大家可以自己搜索。

对于NFT来说,实际上就是Ethereum协会定义的一个规范,也就是ERC721,其作用是与Fungible Token的ERC20规范一样,通过这个规范来统一接口,使得在Token或者NFT上可以衍生出各种各样的DAPP生态,比如各种Swap、借贷等等各类DAPP。

上面说的可能有点抽象,打个广告举个例子:鄙人为了学习写合约,练手撸了一个去中心化的红包DAPP,实际上就是把微信红包这个产品挪到了区块链上,不同的是红包DAPP塞进红包里的是各种Token。这个红包DAPP之所以支持所有Token,正是因为适配了所有Token都遵循的ERC20规范。

NFT的ERC721规范是从ERC20衍生出来了,有很多相同方法,其下规范方法如下:

// 查询NFT中某个owner拥有的数量
balanceOf(owner)

// 查询NFT中某个编号属于的是哪个人,例如查询某个编号的猴属于哪个owner
ownerOf(tokenId)

// 返回NFT的名字
name()

// 返回NFT的符号
symbol()

// NFT总发行量
totalSupply()

// 返回某个NFT的URI,这个URI就是这个NFT的一切描述信息
tokenURI(tokenId)

// 按index序号返回该owner的所有持有NFT的编号
tokenOfOwnerByIndex(owner, index)

// 按index序号返回NFT的编号
tokenByIndex(index)

// 允许to这个地址可以转移他的tokenId编号的NFT(攸关你的NFT安全!)
approve(to, tokenId)

// 查询tokenId编号的NFT授权给了谁(查询谁可以转走你的NFT!)
getApproved(tokenId)

// 授权或者取消授权operator这个地址转移你这一Collection下的所有NFT(攸关你的NFT安全!)
setApprovalForAll(operator, approved)

// 查询某个operator是否有权转移某个owner的这一Collection中的所有NFT(查询某个地址是否可以转走你这个Collection的所有NFT!)
isApprovedForAll(owner, operator)

// 将from这个地址的tokenID编号的NFT转给to这个地址(需要授权才行)
transferFrom(from, to, tokenId)

以上就是几个主要的ERC721规范的方法,虽然写了注释,但是我猜测大家还是一脸懵。

所以下面给大家通过例子来说明,从NFT的起源Mint开始。

Mint铸造

下面这段代码就是OpenZeppelin上一个简单的如何铸造NFT的代码:

// 铸造NFT的方法,只需要传入铸造给谁,再加上这个NFT的tokenURI即可
function awardItem(address player, string memory tokenURI)
    public
    returns (uint256)
{
    _tokenIds.increment();
    uint256 newTokenId = _tokenIds.current();

    // 将这个新的tokenId与player绑定
    _mint(player, newTokenId);

    // 将tokenURI与这个newTokenId绑定
    _setTokenURI(newTokenId, tokenURI);

    return newItemId;
}

铸造NFT实际上就是往NFT的合约里写入了两个信息:

  • tokenId及其owner
  • tokenId及其tokenURI

有人说NFT又有图片,又有各种属性,怎么这么简单了就铸造出来了呢?

没错,就这么简单!

接下来我们用CryptoApes这个NFT举例给大家看看我们大家看到的各种眼花缭乱的NFT是怎么展示出来的?

Play with NFT contract

我们先来看看如何通过简单的几行代码查询CryptoApes NFT的基本信息:

截图中可以看到,通过向CryptoApes合约调用上述ERC721规范的方法name()symbol()totalSupply(),就能拿到这个NFT的名称、符号和总数分别是:CryptoApesCRAP6969

CryptoApes的合约地址以及NFT编号可以在OpenSea中的URL拿到,见下面截图:

可以看到OpenSea的NFT链接 https://opensea.io/assets/0x29714cafe792ef8b8c649451d13c89e21a0d7f5b/24 ,assets后的第一个地址就是该NFT Collection的合约地址,合约地址后的数字就是该NFT编号

下面我们还是通过几行简单代码调用合约,查询一下这只编号为24的CryptoApes的信息:

可以看到我们通过ERC721规范中的**ownerOf()**方法,就能查到这只CryptoApes的拥有者(没错,就是本人)。

另外通过**balanceOf()**方法就能查到本人总共拥有了4只CryptoApes。

值得注意的是截图中的**tokenURI()**方法,通过它就能获取这个24号NFT的tokenURI。这个tokenURI非常重要!因为里面存储了关于这只猴的所有描述信息(Metadata)。

24号猴的tokenURI是:ipfs://QmWGAFtzyzB6A6gYMnb6838hysHuT2rcV8B98Gmj4T4pyY/24.json,说明是存储在IPFS这个分布式存储上的json文件,我们继续通过这个IPFS的网关地址https://ipfs.io/ipfs/QmWGAFtzyzB6A6gYMnb6838hysHuT2rcV8B98Gmj4T4pyY/24.json)访问获取里面的内容:

上面截图中就是这只24号猴的所有信息了,包括名字、图片、描述、编号、创建时间、还有背景颜色、毛颜色、嘴巴衣服等各种各样的属性。(大家点开大图就能看见)

其中的图片信息又指向了另外一个IPFS地址:ipfs://QmZFnUm3bjSyEPrvxEa3fR9eUxnkfQeLmPTzDhAmCWtbMZ/24.png,通过这个地址https://ipfs.io/ipfs/QmZFnUm3bjSyEPrvxEa3fR9eUxnkfQeLmPTzDhAmCWtbMZ/24.png)就能看到24号猴的图片了,这也是OpenSea平台中给大家展示的图片样子。

大家可以看到NFT的所有关键信息都是存在tokenURI这个链接中的,这个链接中的数据包括了你NFT的编号、属性、图片或视频,所以tokenURI这个链下数据能不能正常访问,会不会被篡改非常重要!!!

这里CryptoApes的TokenURI用的是IPFS协议,IPFS协议可以保证不会被篡改。但如果是HTTP协议则有被篡改的可能,虽然使用HTTP协议可以通过checksum校验数据等方式验证,但这并不优雅,并且也不是ERC721的协议规范。

这里插一句,鄙人原本打算写一篇介绍去中心化存储的文章的,因为感觉这个领域的重要性被大家严重忽视了,它也是区块链生态的基础设施,重要性并不比被热捧的各类公链项目低。使用IPFS协议存储内容虽然不会被篡改,但是IPFS还存在它自身的问题,这里就不展开了。

接着说,上面我们查到了编号24的猴的所有关键信息。那如果想把所有6969只CryptoApes的信息查找出来可以吗?

当然可以!使用上述ERC721规范中的“tokenByIndex()”方法进行遍历就行,有兴趣的朋友可以自行试试。

实际上OpenSea支持所有的ERC721的NFT的信息展示,就是通过上述方法去链上抓取数据,并在OpenSea自己的系统中来建立起来所有资源信息,最后通过Web的形式展现到大家面前,方便大家浏览。

这里需要吐槽的是OpenSea网站的robust及performance做得实在太烂了,经常动不动就挂掉,所以鄙人也正着手搞一个类似的产品,从blockchain上同步NFT数据,以及从OpenSea上同步交易数据,存储后按用户需求展示给用户,提供稳定、高效以及易用的浏览及查询服务。(做得好的话未来想象空间也很大,哈哈)

上面通过脚本调用合约给大家展示的都是“”方法,“”方法的话就是在代码中加载钱包,签名之后就可以了。(所以挺简单的吧,大家也别被所谓“科学家”这个名字唬住了。简单学习一下编程,再熟悉下合约的调用,就可以通过程序操作多个钱包,去薅羊毛还是干点别的事情就都可以了

需要警惕的一些方法

ERC721规范中有两个方法需要大家警惕一下,如果不小心也许就会丢掉你的NFT

第一个需要警惕的方法是“approve(to, tokenId)”,这个方法是授权“to”这个地址有权利可以转走你这个“tokenId”的NFT。如果你在小狐狸中授权钓鱼网站调用了这个方法,最多会损失一个NFT。

这个方法ERC20规范中也有,ERC20是授权“to”地址最多可以使用多少数量的token。鄙人的去中心化红包项目,就是在你发红包之前要求你使用“approve”授权一下,好让红包合约可以扣你的token并装入红包中给大家去抢。各类Swap的DAPP也是一样,需要先“approve”才可以进行swap。

第二个需要警惕的方法是“setApprovalForAll(operator, approved)”,这个方法是授权“operator”这个地址可以转走你在这个Collection下所有的NFT。如果你在小狐狸中授权钓鱼网站调用了这个方法,则可能丢失这个Collection下的所有NFT

在OpenSea平台中,如果我们“Sell”一个NFT,小狐狸就会弹出这个方法的授权,见下面截图。

注意红框里的内容,就是向CryptoApes的合约地址调用“setApprovalForAll(operator, approved)”这个方法。授权之后,如果有人出价购买,OpenSea的交易合约则可以把你的NFT直接转给买家,转给买家这个操作调用的是ERC721规范中的方法“transferFrom(from, to, tokenId)”,from是你自己,to是买家,tokenId是这个NFT的编号。

所以大家在小狐狸中进行授权的时候,一定有安全意识,看见approve或者setApprovalForAll方法时一定要注意是不是正规的网站,合约地址是不是正确的地址。如果不小心授权错了,你的NFT就可能被转走。

也许有人会问:我只卖出一个NFT,为什么不单独授权这一个出售的NFT(approve),而是要授权所有的NFT呢(setApproveForAll)?

答:OpenSea的解释是可以省gas费,一次授权后,再次卖出其余NFT的时就不需要因为再次授权而付更多的gas费用了。

也许有人会问:那我授权给了OpenSea所有NFT的转移权限,那OpenSea平台会不会悄悄转走我的NFT呢?

关于这个这个问题可以继续看后面。

OpenSea买卖流程的背后

卖出NFT的背后

下图是在你卖出NFT时候的弹窗截图:

在OpenSea上进行卖出操作时,会弹出窗口第一步让你先初始化钱包(这个是一次性操作),在你付了gas费之后,OpenSea的Registry合约会帮你创建一个钱包合约(实际上就是一个Proxy合约),大家如果在etherscan上查的话,可以看到一个RegisterProxy的操作,实际上调用的就是下面代码去创建了一个属于你个人钱包合约:

function registerProxy()
        public
        returns (OwnableDelegateProxy proxy)
    {
        require(proxies[msg.sender] == address(0));
        // 创建一个新的代理合约
        proxy = new OwnableDelegateProxy(msg.sender, delegateProxyImplementation, abi.encodeWithSignature("initialize(address,address)", msg.sender, address(this)));
        proxies[msg.sender] = proxy;
        return proxy;
    }

为什么需要创建这个合约呢?主要目的是为了安全,因为第二步“Approve this item for sale”时需要授权一个合约地址可以转移你的NFT(也就是上一段落说的setApprovalForAll方法),授权可以转移你NFT的地址,就是“Initialize your wallet”这一步所创建的钱包合约地址。

也就是说OpenSea不能直接转走你的NFT,只有你在OpenSea上初次创建的这个钱包地址才可以转走。

在创建完钱包以及授权NFT之后,如果你去挂出同个Collection下的NFT进行卖出时,不需要额外的手续费,OpenSea仅仅验证你的签名就可以挂出卖单了,这点就是被OpenSea宣传的“gas-free listing”。

所以讲到这里,也许有人又注意到了一个问题:为什么后续挂出卖单仅仅只需要签名,不需要 transaction呢?OpenSea的卖单信息只是存到它自己的中心化服务器上,没有存在链上吗?

答:是的,卖单信息只存在OpenSea的中心化服务器上,没有上链,具体可以参考这里OpenSea的解释。

On OpenSea, most actions are off-chain, meaning they generate orders that are stored in the our system and can be fulfilled by a matching order from another user.

When a user lists an item for sale, they simply sign their intent to swap the item for payment. This intent is stored in the OpenSea system as a sell order, and does not create a transaction.

关于很多评论说OpenSea太过中心化这个问题,之后鄙人也会简单谈谈自己观点。

买入NFT的背后

在OpenSea买入的时候,买卖的撮合其实是发生在OpenSea的中心化系统中,匹配好订单后让用户调用OpenSea交易合约地址AtomicMatch方法完成交易,这个方法里完成了一系列复杂操作,这里就不展开讨论了。

也就是说成交之后,成交订单的信息会上链,毕竟这涉及到了Token和NFT的转移。

OpenSea使用的交易合约应该是Wyvern协议,实际上如果整个交易过程中只有最终交易数据才上链的话,可以不用这么复杂的合约,不过这应该是历史遗留包袱。

大家只需要知道这次买入交易成功的背后,会完成这两个步骤:

  1. 把NFT转给买家
  2. 把买家的钱转给卖家和OpenSea(平台手续费)

其它交易方式

关于Offer报价以及Auction拍卖的交易方式鄙人没有试过,不过我认为跟Listing交易一样,在offer或者bid之前approve一下你的WETH就行,而auction拍卖订单和Offer报价订单的创建与撮合也应该还是通过OpenSea的中心化系统完成。

关于OpenSea太过中心化的评论

鄙人在网络上看到很多声讨OpenSea太过中心化的评论,这段时间开始买NFT对OpenSea进行研究了之后才了解这些评论的根本原因,实际上OpenSea也是基于当时条件限制下的产物。

鄙人这里也为OpenSea的部分中心化技术方案辩驳几句:

  • 首先它降低了手续费,贵族链绝非浪得虚名的。如果所有数据信息上链那必然会导致交易的成本上升,更加提高了用户的交易门槛。(如果所有信息上链的话,我想唯一的好处就是鄙人不会每天收到批量offer的邮件通知了,毕竟每一次offer都需要燃烧gas)
  • 其次Etheruem的performance不足以承载大量的transaction,如果每次挂出卖单、修改卖单价格、每次Offer价格等所有信息都上链的话,那会更加进一步推高Ethereum的gas费用。
  • 最后关键信息上链也一定程度上确保了交易的安全和公开。比如你NFT和WETH的授权是给到你的钱包合约的,OpenSea不直接触碰。OpenSea也开放了它的API,所有未上链的订单数据可以通过API获取,好心人可以通过API拿到数据后和链上最终的交易数据进行比对验证。(不过我猜测不存在这样的好心人吧)

最近这一年公链的迅猛发展,gas费过高以及性能的问题以及得到了极大的缓解。如果OpenSea不思进取、不做改进的话绝对会被大多数用户抛弃,最后发展成为贵族NFT市场,比如前几天LooksRare平台也来空投抢用户了。当然这个问题也是Ethereum需要面对的。

关于OpenSea的交易背后本人有两个问题:

  1. 本人发现在OpenSea上Cancel一个Listing订单,也是需要写入区块链的。但我觉得直接在OpenSea的中心化系统中直接Cancel就好了,毕竟Listing的订单信息也没上链啊,为什么Cancel Listing订单的操作需要上链呢?这gas费不是白白浪费掉了吗?
  2. Listing订单价格的修改,只能往低了修改,不能改高了,如果需要报更高价格的话需要先Cancel Listing订单,然后再重新挂一个,而Cancel的时候又得上链浪费一笔gas费用。同样Listing订单数据没在链上,为什么List price都可以往低了改,而不能直接改高呢?

这两个问题本人没太想明白为什么OpenSea这样做,因为个人觉得在中心化系统上取消订单和随意修改订单价格不是很简单的事情吗?

个人不怀好意的猜测是OpenSea系统做得太烂,不愿意让用户随意取消订单和修改价格,因为这样会导致系统的不稳定。所以Cancel Listing这个操作硬要上链,让用户消耗gas,这样用户在挂卖单的时候就需要慎重考虑要不要挂,以及要挂的价格了。

也希望知道原因的朋友不吝赐教!

上面两个问题的解答(2022-01-25更新)

昨天opensea出现了一个问题,很多人高价的NFT以特别低的价格被出售了,损失惨重。本人今天研究了下,顺便这个问题同步更新一下。

在OpenSea官网中有这么一篇文章教用户如何取消订单的,注意截图里的这段话:

大意是如果你挂出过你NFT的卖单,这时你又把你NFT转到另外一个钱包地址,这种情况不会自动取消你NFT的卖单,所以你应该在转走你NFT之前先把卖单取消掉。

而这次出现损失的用户大部分就是这个原因了。例如几个月前, 用户A把他的NFT挂出10ETH的价格出售,当时价格未成交,这时用户A未取消卖单就直接把NFT转到另外的钱包地址(可能是为了安全转到冷钱包)去了。

过了几个月后发现市场价格不错,就把NFT又转回到之前的钱包地址,准备挂单出售,但当刚转回成功的时候就发现NFT被以之前的卖单价格(10ETH)出售了,当前市场价可能是80ETH,造成了严重损失。

之所以可以被10ETH的卖单价格出售,就是因为之前的卖单信息被人盯上了,所以在转回NFT的时候被人立马以10ETH的低价购买成功。

合约分析

上面说过,opensea的卖单信息是中心化存储的,这个卖单信息中包括了你NFT的信息、价格以及签名,同时这个卖单信息可以通过opensea的API查询到。

科学家可以通过opensea的API找到一些价值比较高的NFT的卖单信息,同时监控该NFT是否又被转回到与该卖单信息一致的钱包地址,当发现转回时,科学家调用前文所述的atomicMatch这个方法即可完成交易。

截图中可以看到,atomicMatch方法里有个requireValidOrder内部方法,该方法会去校验这个卖单信息是否有效,签名是否正确等。这个卖单信息是存在opensea上并通过API开放出来的,任何人拿到这个卖单信息并且提供能与之匹配的买单,就能完成交易。

上面这个截图是购买时,requireValidOrder方法里判断卖单信息是否合法的实现,可以看到有一步是通过cancelledOrFinalized这个map类型变量来判断的。

而之前说的取消订单是一个上链操作,就是通过设置cancelledOrFinalized这个状态来控制卖单信息失效的。

总而言之,就是一个用户之前挂过低价卖单,后来NFT转走了就再也没管过这个卖单,而这个高价值NFT的低价卖单信息(包含签名)又被人盯上了,所以当NFT又转回来的时候就可以以之前的低价成交。

本人之所以有上一节的两个问题,完全是因为没看过opensea的API,没想到opensea这么open,把卖单的签名信息也开放出来了,任何第三方拿到这个卖单信息都可以自己下单进行NFT的购买。

如何避免损失?

对于opensea来说,现在这个交易合约是n年前的产物,我觉得可以重新设计一个,做好迁移过度相关的工作,但这个工作量会比较高,费时比较长。我猜opensea已经在计划了,因为opensea上polygon链的NFT已经不再是当前的交易方式。

另外对于opensea来说更简单粗暴的方式就是卖单信息中的签名别开放出去,为了安全更加中心化一些,防止不知情的用户造成损失。

对于用户来说,记得取消卖单,虽然费一些gas费吧。另外如果曾经挂过卖单且没取消过的钱包,就别把NFT往回转了, 就用新钱包直接卖吧。

最后的最后

最近因为PhantaBear的关系对NFT着迷了,鄙人着迷到什么程度呢?我媳妇告诉我,我小学2年级的孩子都在问她同学有没有爆炸头熊的NFT了。。。

着迷到已经有点动摇BTC Hodl决心,盘算着是不是拿点BTC去换一个CryptoPunks的程度,毕竟有NFT第一的故事加持,非常想搞一枚。

关于NFT的胡思乱想

社会的经济活动实际上就是钱和资产相互兑换的过程。比如用钱购买衣服、鞋、车、房就是用钱购买了资产。同样也可以变卖衣服、鞋、车、房将资产换取钱,这就是基本的经济活动。这个交易过程的顺利执行其实是通过合同保证的(买衣服、鞋这类小商品虽然没有实体合同,但实际上存在虚拟合同),而社会法律强制确保了合同可以按预期执行(你要是收了钱但没给我东西,我就可以起诉你)。

你会看到在区块链的世界中是一模一样的,BTC、ETH等各种币就是钱,而玲琅满目的NFT就是个各类资产。crypto可以购买NFTNFT可以变卖换成crypto,这样就形成了未来元宇宙中的经济活动。交易通过智能合约来完成,而公链的共识机制又确保了智能合约能够以预期方式执行。

你会发现区块链行业的发展与现实社会极其相似,NFT的出现也许不是偶然吧。

不过比较有意思的是,人类社会最早的经济活动只是以物换物,例如我用羊去换你的牛,因为以物换物的不便才出现了货币作为交换的媒介。所以现实社会是先出现资产,再出现货币。但区块链行业中先出现的是各类加密货币,然后才出现了代表资产的NFT,与现实社会相反

投资建议声明

以上文章内容全是鄙人胡说八道,不作为任何投资建议。

求关注

关注我一定不会让你暴富,但也许让你避坑。如果你想也许能避坑的话,请关注我的twitter吧。本人写的历史文章链接放在下方,供各位参考:

学习调研系列:

也许会踩坑系列:

彩蛋

最后能看到这里的读者,真心感谢你们,满篇枯燥的技术描写以及鄙人的胡思乱想能让你们坚持读到这里挺不容易的。

在我购买了四个CryptoApes的NFT之后,Discord里有个热心的 @bhodl 主动帮我加上了眼睛放光动画效果,**十分酷炫!**所以我把他加了效果之后的GIF图做成了四个NFT,作为本篇文章的彩蛋抽给读者,这四个NFT的链接可以看这里

这里的抽奖规则如下:

  1. 在我这篇文章的twitter里点赞、转发、关注并留下你BSC的钱包地址
  2. 注意我推特上的通知,到时候我会发一个红包,twitter中留下BSC钱包地址的人可以来抢该红包(注意因为是在BSC上的去中心化红包DAPP,所以你需要准备一点BNB作为gas费)
  3. 抢到这个红包中的朋友,且拿到红包里钱最少的四位朋友,就是中奖者

届时我会把这四个NFT直接转到那四位中奖者的钱包地址。(转NFT的地址必须与红包DAPP中的记录相同。与别的推特抽奖活动不同,我这所有的抽奖活动都是公开公正的,且可以在区块链上追踪验证的。)

好了,最后谢谢各位啦。

Subscribe to Xing
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.