上篇写NBA的文章写的太累了大伤元气,想休息一段时间再写的,结果web3的世界实在是太精彩,每天发生的大新闻太多,大周末的又被迫营业。
今天一个叫Akutar的NFT项目因为合约bug,导致11539个ETH,价值3400万美金2个亿人民币的钱永久取不出来被锁死了,2个亿啊!
我们打开合约地址看看这2个亿来眼馋眼馋,想象一下Akutar团队望着这一串数字的抱头痛哭的心情。
首先介绍一下Akutar,从官网的描述和他们twitter可以看出,这不是一个土狗项目,相反是一个很用心的高质量项目,不论是从精细的画风还是roadmap描述质量都很高。
它的发起人是一位知名的棒球运动员Micha Johnson,起源于他无意间听到一位黑人小男孩与母亲的对话,小男孩问母亲宇航员能否是黑人,所以Micha Johnson决定发行一系列梦想成为宇航员的戴着头盔的黑人小男孩,一个还算美妙的故事。
那么看着这么暖心的故事背后的NFT这么就砸了呢?从某种程度上还是项目方对于赚钱的渴望大过于所谓的暖心公益,从而搬起石头砸了自己的脚,因为它使用了一种比较独特的荷兰拍方式。
传统的拍卖方式是设置一个低价,然后大家向上叫价,最终出价最高者可购买,这是英式拍卖,荷兰式拍卖则是先设置一个最高价,然后逐渐的降价,最终有人在某个价格点出手将其买下来,荷兰拍更考验人性,因为每个人都想等最低价,但是都怕别人先于自己购买。
Azuki就是用的是荷兰拍,但是Akutar相比于Azuki的拍卖方式又做了改变,Azuki的价格是动态下降的,从而买的越晚价格越低,买的越早可能就吃了亏价格会高,Akutar则加了一个“退款”规则,该规则看起来好像对用户更友好但是我认为实际上是想割更多的钱。
这如下图所示,拍卖起始价格是3.5ETH,每过6分钟降低1ETH,最终最低价格购买的人将成为标准价格,其他高于该价格购买的用户将获得退款,比如最后最低出售价格是1.5ETH,那所有高于1.5ETH出价的人均会获得差价的退款,这种实际上是想让用户放心大胆的去买,不要蹲守最低价,即使买高了也能退款。
所以Akutar会有一个巨大的资金池用于存储所有用户交的钱,这部分钱包括项目方自己应得的,也包括需要退给用户的,这里先科普一个知识,之前的文章中也提到过,智能合约的性质和你自己个人的钱包地址是一样的,都是一个区块链地址,可以接收、发送虚拟货币,当你在mint某个项目时,实际上是你先将钱打到项目合约地址,然后合约给你转一个NFT,即所有NFT的一级市场发售,钱都是先到了合约地址,再由项目方去进行提款操作,将合约里面的钱提到自己的钱包中。
这次2个亿被锁死就是因为在提款这个步骤出了bug,因为区块链智能合约不可篡改的特性使得出现了bug是没法修的,传统互联网如果有个bug导致钱取不出来,修复迭代就可以,但是在web3中意味着这辈子你只能与这2个亿隔空相望。
我们来看一下一些关键的代码都做了什么帮助大家理解原理,再分析到底是哪里出了问题。
我们先学习一下荷兰拍的原理,首先是获取当前价格,这里先获取了最新区块的时间block.timestamp,然后用当前时间减去开始时间startAt并除以6(因为每6分钟降价一次),从而获取应该降价几次timeElapsed,再用降价次数乘以降价金额计算出降价的总数discount,最终用起始价格startingPrice减去降价金额得到当前应该要支付的费用。在代码中刚才提到的这些涉及到金额的参数其实都不是预先写在合约中的,而是可以修改的变量,说明项目方给自己留了后门可以视情况随时修改金额从而更好的挥舞镰刀。
怎么获取价格清楚了,我们再看用户出价的过程都发生了什么,这部分代码太长了我就不都贴了,挑重要的讲。
先获取了上面提到的当前价格,然后乘以用户购买的数量amount,得到应该支付的总价totalPrice,再判断用户实际支付的价格value是否大于总价,如果大于说明钱给够了接着向下执行。
这里先定义了一个报价bid都包含了什么,分别是bidder报价者地址,price具体报价,bidsPlaced总共购买数量,和finalProcess退款状态,0是退款,1是已退款,2是取消退款。
接下来到了第一个埋坑的地方: totalBids表示当前所拍卖出去的NFT数量,默认是0,每次有用户报价则加上用户要购买的数量amount,记住这里,等会会用到。
然后埋了第二个坑:使用了一个叫bidIndex的参数用于存储产生报价的用户有多少人。记住这两个参数,totalBids存储了总共卖出多少个NFT,bidIndex存储了总共有多少人买了NFT。
再讲一下项目方为用户退款的过程,项目方要先点击一个叫processRefunds的按钮开启退款,这个按钮背后的逻辑是把所有出价的用户全部循环处理一次,循环的次数就是刚才说的存储出价人数的bidIndex。处理的内容是先判断该用户finalProcess退款状态是否为0,0表示尚未退款,如果为0的话继续向下执行,将用户当时的报价减去最低成交价,再乘以购买数量,则等于要退给用户的差价refund。
然后将该finalProcess用户退款从0设置为1,表示已经完成退款,从而该用户不能再去退了。
参数refundProgress是记录完成退款人数的,每退完一个用户就会加1,因为是按照出价人数bidIndex循环的,所以refundProgress和bidIndex是一致的,这里其实没有毛病,本来出价的人和退款的人就应该是一样的,但是!接着向下看!
项目方提款的逻辑是怎样的,又有什么漏洞导致其无法提款?
下图为项目方进行提款的函数,即当项目方点击claimProjectFunds按钮后可以将钱提到自己钱包里,这里有三层校验,第一层是先验证当前是否已经结束了拍卖,如果结束进入第二层校验退款人数是否大于报价人数,其实这里项目方是好意,因为要确保每个人都退完了钱,项目方再提款,但就是这一层校验出了问题,不知道你还记不记得totalBids是什么意思?是售出NFT数量呀,不是报价人数!
你会问那这又怎么了呢?一个人在报价的时候是可以购买多个NFT的呀,退款人数实际上是购买人数,你要求购买人数超过卖出NFT数量,但是每人又可以买多个,那只要有1个人买了2个,就意味着购买人数永远不可能大于卖出数量,10个人卖出了11个,你怎么要求10大于11呢?
我们上etherscan看一下,refundProgess的数量是3699,说明共有3699人报价,但是totalBids的数量是5495,即共卖出了5495个,远远超过3699,这辈子refundProgess都不可能大于totalBids,这2个亿就永远被锁死在了合约中供后人观摩。
所以是项目方写错单词了,本来应该是想写bidIndex购买人数,结果写成了totalBids卖出数量,一个单词价值2个亿,这应该是全世界最贵的一个单词了,大家给我狠狠的记住这个单词totalBids,就是它值2个亿!
通过这篇文章带着大家学习了一种新的mint方式荷兰拍以及其原理,另外带大家认识了一个2个亿的单词totalBids。