Akutar NFT的3400万美金因为写错一个单词被永久锁死了?!

今天一个叫Akutar 的NFT 项目因为合约bug,导致11,539 个ETH,价值3,400 万美金、2 个亿人民币的钱永久取不出来被锁死了,2 个亿啊!

上篇写NBA 的文章写的太累了大伤元气,想休息一段时间再写的,结果web3 的世界实在是太精彩,每天发生的大新闻太多,大周末的又被迫营业。

我们打开合约地址看看这2 个亿来眼馋眼馋,想像一下Akutar 团队望着这一串数字的抱头痛哭的心情。

首先介绍一下Akutar,从官网的描述和他们twitter 可以看出,这不是一个土狗项目,相反是一个很用心的高质量项目,不论是从精细的画风还是roadmap 描述质量都很高。

它的发起人是一位知名的棒球运动员Micah Johnson,起源于他无意间听到一位黑人小男孩与母亲的对话,小男孩问母亲宇航员能否是黑人,所以Micah Johnson 决定发行一系列梦想成为宇航员的戴着头盔的黑人小男孩,一个还算美妙的故事。

那么看着这么暖心的故事背后的NFT 这么就砸了呢?从某种程度上还是项目方对于赚钱的渴望大过于所谓的暖心公益,从而搬起石头砸了自己的脚,因为它使用了一种比较独特的荷兰拍方式。

传统的拍卖方式是设置一个低价,然后大家向上叫价,最终出价最高者可购买,这是英式拍卖,荷兰式拍卖则是先设置一个最高价,然后逐渐的降价,最终有人在某个价格点出手将其买下来,荷兰拍更考验人性,因为每个人都想等最低价,但是都怕别人先于自己购买。

Azuki 就是用的是荷兰拍,但是Akutar 相比于Azuki 的拍卖方式又做了改变,Azuki 的价格是动态下降的,从而买的越晚价格越低,买的越早可能就吃了亏价格会高,Akutar 则加了一个「退款」规则,该规则看起来好像对用户更友好但是我认为实际上是想割更多的钱。

这如下图所示,拍卖起始价格是3.5 ETH,每过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 的数量是3,699,说明共有3,699 人报价,但是totalBids 的数量是5,495,即共卖出了5,495 个,远远超过3,699,这辈子refundProgess 都不可能大于totalBids,这2 个亿就永远被锁死在了合约中供后人观摩。

所以是项目方写错单词了,本来应该是想写bidIndex 购买人数,结果写成了totalBids 卖出数量,一个单词价值2 个亿,这应该是全世界最贵的一个单词了,大家给我狠狠的记住这个单词totalBids,就是它值2 个亿!

通过这篇文章带着大家学习了一种新的mint 方式荷兰拍以及其原理,另外带大家认识了一个2 个亿的单词totalBids。

Leave a Reply

Your email address will not be published. Required fields are marked *