# Malicious royalty recipient can steal excess eth from buy orders

EthRouter.buy refunds surplus ETH before paying royalties, letting a malicious ERC-2981 royalty recipient reenter buy with empty buys and drain the router's ETH balance.

- Payout: Contest reward
- Severity: medium
- Program: Caviar
- Credited: Voyvoda
- Disclosed: 2023-05-18
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2023-04-caviar-findings/issues/569
- Library page: https://bugbountylibrary.com/r/c4%3A2023-04-caviar%3AM-09
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:chained technique:source-review vuln:business-logic vuln:reentrancy

**Target:** EthRouter.buy / PrivatePool.buy ETH refund and royalty payout
**Root cause:** PrivatePool.buy refunds excess ETH to the caller before transferring royalty fees to the recipient, and EthRouter.buy lacks a reentrancy guard, so the royalty recipient's fallback can reenter and sweep the router's balance.
**Steps:**
1. User submits Buy order(s) via `EthRouter.buy(Buy[] buys, uint256 deadline, bool payRoyalties)` sending excess ETH.
2. EthRouter forwards to `PrivatePool.buy`, which refunds excess ETH to EthRouter before paying royalty.
3. PrivatePool calls `recipient.safeTransferETH(royaltyFee)` on the malicious ERC-2981 royalty recipient.
4. Recipient's fallback reenters `EthRouter.buy` with empty `buys`, `deadline=0`, `payRoyalties=false`.
5. Empty loop is skipped; `if (address(this).balance > 0) msg.sender.safeTransferETH(address(this).balance)` sends all router ETH to attacker.
6. Original buy continues; attacker keeps stolen excess ETH (works cleanly when malicious tokenId is last in the last Buy).
**How found:** Code reading of PrivatePool.buy ordering: refund-before-royalty-payout combined with EthRouter.buy's surplus-refund branch and no reentrancy guard; noted ERC-2981 royaltyInfo returns an arbitrary recipient address.
**Exploitation:** Attacker registers as royalty recipient via ERC-2981 royaltyInfo, waits for a victim Buy order routed through EthRouter, and on royalty payout reenters EthRouter.buy with empty buys to sweep the router's ETH balance mid-transaction.
**Escalation:** For non-last tokenId positions, attacker can front-run/mempool-inspect and craft fallback to return just enough ETH to let remaining buys complete, still netting the excess.
**Insight:** When a router holds user ETH and refunds surplus based on `address(this).balance`, any external call (royalty payout, NFT transfer hook) before that refund is a reentrancy vector to sweep pooled funds; check ERC-2981 recipient as attacker-controlled.
