2022年12月9日,ETH链上Jay合约遭受重入攻击,代币合约地址及代码:
https://etherscan.io/address/0xf2919d1d80aff2940274014bef534f7791906ff2
问题的原因出在合约中的buyJayWithERC721()函数,该合约中调用了未检测的外部合约函数并且未检测balance状态的修改
先看一下jay代币的定价方式,池子中有一定数量的jay和eth。假设为totalsupplyJay和reserveEth。当用户需要用AmountEthIn的数量购买jay的时候,用户可以买到的jay数量为:
同样的。当用户打算卖出AmountJayIn数量的Jay并获取ETH是,他可以换到的ETH数量为:
接下来我们看下出问题的点,也就是函数buyJayWithERC721
function buyJayWithERC721(
address[] calldata _tokenAddress,
uint256[] calldata ids
) internal {
for (uint256 id = 0; id < ids.length; id++) {
IERC721(_tokenAddress[id]).transferFrom(
msg.sender,
address(this),
ids[id]
);
}
}
IERC721(_tokenAddress[id]).transferFrom(msg.sender,address(this),ids[id])这里的_tokenAddress[id]是用户可控的参数,触发点位于:buyJay函数:
// Sell NFTs (Buy Jay)
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));
}
所以目前攻击者可以做的事是:
调用buyJay合约,自定义参数address[] calldata erc721TokenAddress为任意的一个ERC721合约。
接下来看回公式:
如果用户想要获得更多的AmountEthOut,需要做的就是
增加AmountJayIn
减少totalsupplyJay
增加reserveEth
而增加reserveEth是实现起来最简单的方案:只需要在call的时候代入eth就可以了。配合重入漏洞我们就可以做到:
Jay.buyJay(erc721contract){value:xxxxETH} --> buyJayWithERC721(erc721TokenAddress, erc721Ids) --> IERC721(_tokenAddress[id]).transferFrom() --> erc721TokenAddress.transferFrom() -->
Jay.sell()
在调用sell的时候 由于Jay.buyJay(erc721contract){value:xxxxETH}传入了ETH,这时也就增加了adress(Jay).balance (reserveETH)。获取了更多的ETH。之后再卖掉xxxxETH兑换来的Jay即可。
以下是真实的攻击交易详情:用户通过闪电贷借出了72.5个ETH。先通过22ETH买入了13584899853779845952188个Jay。然后再次调用buyJay(){50.5ETH}的通过重入直接调用sell函数卖出13584899853779845952188 ,操纵reserveETH达到太高Jay价格的效果。

