# Direct theft of buyer’s ETH funds

Malicious fee recipient reenters bulkExecute with empty array during fee payout, draining buyer's remaining ETH via _returnDust's selfbalance transfer.

- Payout: Contest reward
- Severity: high
- Program: Blur Exchange
- Credited: 0xdeadbeef0x
- Disclosed: 2022-12-08
- Write-up quality: 9/10
- Original: https://github.com/code-423n4/2022-11-non-fungible-findings/issues/96
- Library page: https://bugbountylibrary.com/r/c4%3A2022-11-non-fungible%3AH-01
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:accounting-error vuln:reentrancy

**Target:** Exchange.sol bulkExecute/_returnDust ETH accounting
**Root cause:** bulkExecute lacks a reentrancy guard on the outer function and accepts an empty executions array, so a fee recipient reentering during _transferFees skips _execute's guard and reaches _returnDust, which sends selfbalance() instead of remainingETH.
**Steps:**
1. Deploy malicious fee recipient contract with receive() that reenters Exchange.bulkExecute with empty Execution[] and 1 wei.
2. Seller lists NFT with fee recipient set to attacker contract at 10% (or 100%) rate.
3. Buyer calls `bulkExecute(executions, {value: 4 ETH})` for 4 orders.
4. _executeFundsTransfer pays fee to attacker contract, triggering receive().
5. Attacker reenters `bulkExecute(new Execution[](0))` with 1 wei; no _execute runs so reentrancyGuard not hit; _returnDust sends selfbalance() (~3.9 ETH) to attacker.
6. Attacker forwards stolen ETH to seller minus amount needed, then selfdestructs to force-send 0.9 ETH back to Exchange so _executeFundsTransfer succeeds.
7. Remaining _execute calls revert; buyer loses ~3 ETH with no refund.
**How found:** Code review of new bulkExecute/setupExecution accounting: noticed _execute has reentrancyGuard but bulkExecute does not, empty array skips _execute entirely, and _returnDust sends selfbalance() rather than remainingETH.
**Exploitation:** Attacker-controlled fee recipient reenters bulkExecute with empty array during fee transfer, drains contract balance via _returnDust's selfbalance() call, uses selfdestruct to top up Exchange so the current _execute completes, leaving subsequent executions to revert and buyer funds stolen.
**Escalation:** Setting fee rate to 10000 (100%) removes the need to refund Exchange via selfdestruct, simplifying the heist.
**Insight:** When a contract adds a batch wrapper around a guarded internal function, check whether the wrapper itself is guarded and whether zero-length inputs bypass the guard; also audit refund helpers that use selfbalance() instead of tracked accounting.
