UTU 2022 年 12 月 29 日 5 时(北京时间 13 时),以太坊主网上的 JayPeggers 协议遭到重入攻击,损失约 15 ETH。随后攻击者通过 Tornado.CashAztec 转移被盗资金。

分析

首先,攻击合约从 Vault(0xba12222222228d8ba445958a75a0704d566bf2c8) 获得借出 72.5 WETH 的代币余额,然后从 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 利用 withdraw 函数取出。至此,攻击者通过闪电贷获得了起始资金。

接着调用了 buyJay,关键在于计算铸币数量的 ETHtoJAY 函数,它是用 代币总量/ETH 总量,计算代币价格的。这次只是通过 22 WETH 购买了 13584899853779845952188 Jay 代币。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function buyJay(
address[] calldata erc721TokenAddress,
uint256[] calldata erc721Ids,
address[] calldata erc1155TokenAddress,
uint256[] calldata erc1155Ids,
uint256[] calldata erc1155Amounts
) public payable {
require(start, "Not started!");
uint256 total = erc721TokenAddress.length;
if (total != 0) buyJayWithERC721(erc721TokenAddress, erc721Ids);

if (erc1155TokenAddress.length != 0)
total = total.add(
buyJayWithERC1155(
erc1155TokenAddress,
erc1155Ids,
erc1155Amounts
)
);

if (total >= 100)
require(
msg.value >= (total).mul(sellNftFeeEth).div(2),
"You need to pay ETH more"
);
else
require(
msg.value >= (total).mul(sellNftFeeEth),
"You need to pay ETH more"
);

_mint(msg.sender, ETHtoJAY(msg.value).mul(97).div(100));

(bool success, ) = dev.call{value: msg.value.div(34)}("");
require(success, "ETH Transfer failed.");

nftsSold += total;

emit Price(block.timestamp, JAYtoETH(1 * 10**18));
}
1
2
3
function ETHtoJAY(uint256 value) public view returns (uint256) {
return value.mul(totalSupply()).div(address(this).balance.sub(value));
}

攻击者第二次调用 ETHtoJAY 函数,这次用 50.5 ETH 铸造了 4313025058290613910965927 Jay 代币。

1
2
3
4
5
6
7
8
9
10
11
{
"erc721TokenAddress": [
"0xed42cb11b9d03c807ed1ba9c2ed1d3ba5bf37340"
],
"erc721Ids": [
"0"
],
"erc1155TokenAddress": [],
"erc1155Ids": [],
"erc1155Amounts": []
}

可以知道,售卖的 NFT 是一个攻击者自定义的 ERC721 合约,实际上没有任何价值,只是用来通过第 10 行的 buyJayWithERC721 重入的。

image-20221229232347867

sell 函数中,计算 eth 的逻辑如下:

1
2
3
function JAYtoETH(uint256 value) public view returns (uint256) {
return (value * address(this).balance).div(totalSupply());
}

这时 address(this).balance 已经因为传入了 50.5 WETH 而增大了,但是buyJay 中的 _mint 函数中的 _totalSupply 还没变化,因此造成了价格操控。