这个bug是dtf发现的,在他们提供的审计报告1中,描述了这个漏洞,评级为严重(severe)。
简单说一下这个bug会带来什么问题:一个punk主可以做损人不利己的事。
损人不利己这种事,虽然很少,但确实时有发生,不然怎么会有这个词呢。
一个punk主如何做到这点:
他把自己的punk转移给地址0,那个当前竞标此punk出价最高的人,就会失去自己在该合约账户上的竞标资金,因为资金被锁住了。
我们看一下在代码上这个bug是怎么实现的。
注:如果你毫无概念,最好先看下这篇文章:《规则即代码:人话解读加密朋克智能合约》
1.某用户通过enterBidForPunk进入竞标,如果出的是最高价,其资金就会进入合约。
在合约中,这段代码是下面这个样子的(可以不看)。
//用户对某个punk开始投标,参数是punk编号,调用时要带钱的。函数接受Ether。
function enterBidForPunk(uint punkIndex) payable {
if (punkIndex >= 10000) throw; //punk编号应小于10000
if (!allPunksAssigned) throw; //特权派送结束后才可以
if (punkIndexToAddress[punkIndex] == 0x0) throw; //该punk必须是有主的,而不是无主的
if (punkIndexToAddress[punkIndex] == msg.sender) throw; //该punk的主人不能参与竞标
if (msg.value == 0) throw; //投标价格一定要大于0
Bid existing = punkBids[punkIndex]; //获取目前的投标信息,目的是获取当前最高竞标价
if (msg.value <= existing.value) throw; //出的投标价高于当前的最高价时,该投标才成功
if (existing.value > 0) {//之前投标的人的投标价会返回到它的临时账户中
// Refund the failing bid
pendingWithdrawals[existing.bidder] += existing.value;
}
punkBids[punkIndex] = Bid(true, punkIndex, msg.sender, msg.value); //更新该punk的投标信息
PunkBidEntered(punkIndex, msg.value, msg.sender); //公告有人以最高价竞标这个事件
}
2.恶意punk主将自己的punk转移给地址0x0,事实上,这等于销毁了这个punk
为什么将punk转给0x0将相当于销毁,可以想一下?
因为没人拥有0x0这个地址,要想算出等于0的以太坊地址,以当前的科学技术,不可能。
注:以太坊地址算法:通常是随机产生私钥,然后产生公钥,然后用Keccak-256算法对公钥做哈希,取big-endian顺序的后20个字节作为以太坊地址。
把punk转给一个没有对应私钥的地址,就相当于丢失了它,所以被称为销毁(burn)。
而加密朋克的代码,并没有说不能转给0x0地址。
下面代码中,箭头所指那行完成这个动作。
这就实现了“损人不利己”之不利己
。
// 转移punk给他人,参数:转给谁(to),punk的编号
function transferPunk(address to, uint punkIndex) {
if (!allPunksAssigned) throw; // 必须是特权派送结束才行
if (punkIndexToAddress[punkIndex] != msg.sender) throw; //函数调用者必须是punk的主人
if (punkIndex >= 10000) throw; // punk编号要大于10000
if (punksOfferedForSale[punkIndex].isForSale) { //如果之前主人正在售卖这个punk
punkNoLongerForSale(punkIndex); //停止售卖
}
---> punkIndexToAddress[punkIndex] = to; // 记录这个punk归to了
balanceOf[msg.sender]--; // 主人拥有数减一
balanceOf[to]++; // to的拥有数加一
Transfer(msg.sender, to, 1); // 记录punk转移事件,记录转移人、被转移人
PunkTransfer(msg.sender, to, punkIndex); // 记录punk赠送事件(其他的转移是花钱的)
// Check for the case where there is a bid from the new owner and refund it.
// Any other bid can stay in place.
//查看被转移方是否之前有对该punk进行投标,有则取消,并将其投标的金额放进其临时账户中
Bid bid = punkBids[punkIndex];
if (bid.bidder == to) {
pendingWithdrawals[to] += bid.value; // 归还投标者在合约中锁定的金额
punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); //清空投标信息
}
}
3. 竞标用户将无法退出投标,也即钱被锁定在合约里再也拿不出来了。
用户原本是可以通过withdrawBidForPunk来取消投标得到自己资金返还的。
现在可好,请看下列代码箭头所指之处,由于该punk现在对应的地址是0,所以这个函数是执行不下去的(throw就退出了)。
所以,他的资金就无法返回,而且是谁也动不了了。
这就是损人不利己中的损人
。
// 投标人可以自行放弃对某个punk的投标,参数:punk编号
function withdrawBidForPunk(uint punkIndex) {
if (punkIndex >= 10000) throw; // punk编号应小于10000
if (!allPunksAssigned) throw; // 特权派送结束后才可以做这事
---> if (punkIndexToAddress[punkIndex] == 0x0) throw; // 该punk应该是有主的
if (punkIndexToAddress[punkIndex] == msg.sender) throw; //该punk的主人不能干这事
Bid bid = punkBids[punkIndex]; // 获取当前的投标信息
if (bid.bidder != msg.sender) throw; // 如果调用者不是当前最高出价人,退出。
PunkBidWithdrawn(punkIndex, bid.value, msg.sender); // 公告这个放弃投标事件
uint amount = bid.value; // 获取当前投标价(也即最高投标价)
punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); // 清空投标信息
msg.sender.transfer(amount); // 将投标资金返还给投标者的以太坊地址
}
代码修改方法
其实很简单,要么在取回资金时,允许该punk无主;要么在转移punk时,不允许转移给0x0。
但已经部署的合约是不能再改了,再改就要重新部署,而且老合约中已有的数据也需要迁移到新的合约中。
只有Larva Labs官方宣布这么做,这个行为才能被公认。
发生此事的可能性
我认为可能性很小。
毕竟,损人不利己的情况一般发生在对别人有怨恨的情况下:即便自己损失,也要让别人难受。
你说一个punk主怎么会怨恨一个不知道是谁的投标者呢!
不过大千世界,无奇不有,什么事都有可能发生。
我们可以假设这种情况:
有个人是很低成本得到punk的,比如是最初通过getPunk接口获取了一个(花点gas费)。
我随便看了一下,6304号punk就是这样的,punk主几乎没有成本,但投标价现在(2021.11.11)是870ETH。
然而,他想搞一个大事情。
他这么做的动机可能是什么呢?
1、他想通过这件事,警醒世人,区块链编码一定要注意安全性,只要是人编写的代码,就一定会有漏洞。(多么善良的人!)
2、通过此事,极大地打击加密朋克,导致其市值暴跌,然后自己抄底。(还是看好的嘛!)
3、他恨死那个正在竞标的人了。
6304号punk当前(2021.11.11)的最高竞价
https://www.larvalabs.com/cryptopunks/details/6304
责任问题
如果出了这事,谁有责任,什么样的责任?
其实这是加密朋克创始公司Larva Labs最不愿意看到的情况,他们在官网上说过:
“我们编写了存在于区块链上的代码,任何人都可以使用它与世界上任何另一个人买卖朋克(punk)。这个系统一个有趣的地方是,我们不再控制运行加密朋克的代码!一旦我们将其发布到区块链上,它就永久嵌入在那里,任何人都无法再修改。这对我们作为开发人员来说很可怕,因为我们担心有Bug。但不能修改代码这点是非常强大的,它允许用户验证确实只有10000个朋克,验证我们无法从您那里窃取任何一个punk,并确保我们所声称的都是真实的。加密朋克被创造后就运行在以太坊网络上,代码掌控所有权。此后,我们不再有创造上或是所有权上的控制了。”
然而,并不容易判定punk主有罪。
首先,干这个坏事的人,未必能抓住他。
其次,就算是抓住他,他可以辩解说,自己只是试着玩玩(或者不小心失手),没想到把自己的punk给弄没了,更没想到居然还害了别人!
再次,他可以说,我确实是想销毁,但我并没有想害人,我哪知道这代码有漏洞,我没有看过什么审计报告,更没有看过卫sir写的什么文章!
最后,他说,这些我都知道,但我觉得“规则即代码”嘛,我是按照你们的规则在玩啊,我哪里有违规。
加密朋克有没有责任?
Larva Labs会说,我只是写了个程序大家玩玩,没想到你们出这么多钱玩,我这是开源程序哦,我在github上已经说过了,我遵循的是MIT协议,MIT协议怎么说的?
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
笔者用人话翻译一下2:
本软件是“按原样“提供的,不附带任何明示或暗示的保证,包括没有任何有关适销性、适用性、非侵权性保证以及其他保证。在任何情况下,作者或版权持有人,对任何权益追索、损害赔偿以及其他追责,都不负任何责任。无论这些追责产生自合同、侵权,还是直接或间接来自于本软件以及与本软件使用或经营有关的情形。
看见没有,我早说了,我不负责的,莫谓言之不预啊!
后记
希望没有人真干这事。
注:如果不是加密朋克,而是我国的类似产品,要遵循《网络安全法》和《网络产品安全漏洞管理规定》,发现漏洞的人要按照规定报告漏洞,一旦发生恶意利用,利用人、产品提供者都要负相应的责任。
CryptoPunks Quick Audit(https://dtf.is/audits/cryptopunks_dtf_assessment.pdf)
从MIT协议谈契约精神