JAY Reentry攻击分析

2022年12月9日,ETH链上Jay合约遭受重入攻击,代币合约地址及代码:

https://etherscan.io/address/0xf2919d1d80aff2940274014bef534f7791906ff2

问题的原因出在合约中的buyJayWithERC721()函数,该合约中调用了未检测的外部合约函数并且未检测balance状态的修改

先看一下jay代币的定价方式,池子中有一定数量的jay和eth。假设为totalsupplyJay和reserveEth。当用户需要用AmountEthIn的数量购买jay的时候,用户可以买到的jay数量为:

AmountJayOut=AmountEthIn/reserveEthtotalsupplyJayAmountJayOut = AmountEthIn/reserveEth * totalsupplyJay

同样的。当用户打算卖出AmountJayIn数量的Jay并获取ETH是,他可以换到的ETH数量为:

AmountEthOut=AmountJayIn/totalsupplyJayreserveEthAmountEthOut = AmountJayIn/totalsupplyJay*reserveEth

接下来我们看下出问题的点,也就是函数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/totalsupplyJayreserveEthAmountEthOut = AmountJayIn/totalsupplyJay*reserveEth

如果用户想要获得更多的AmountEthOut,需要做的就是

  1. 增加AmountJayIn

  2. 减少totalsupplyJay

  3. 增加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价格的效果。

https://explorer.phalcon.xyz/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6?line=11

post image