# Royalty receiver can drain a private pool

PrivatePool.buy calls _getRoyalty twice with an ETH refund external call between them, letting a malicious royalty receiver inflate the fee and drain the pool.

- Payout: Contest reward
- Severity: high
- Program: Caviar
- Credited: Voyvoda
- Disclosed: 2023-05-18
- Write-up quality: 8/10
- Original: https://github.com/code-423n4/2023-04-caviar-findings/issues/320
- Library page: https://bugbountylibrary.com/r/c4%3A2023-04-caviar%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:** PrivatePool.buy NFT marketplace contract
**Root cause:** buy() computes total royaltyFeeAmount in one loop, then refunds excess ETH via an untrusted external call, then re-reads _getRoyalty per token in a second loop; the royalty fee can change between the two reads.
**Steps:**
1. Attacker deploys contract that owns an NFT with ERC-2981 royalty set to 0 bps and receiver = attacker.
2. Attacker lists/uses a private pool holding base tokens and calls `buy(tokenIds, ...)` with msg.value > netInputAmount so a refund is due.
3. First loop reads royaltyFee=0, so netInputAmount excludes royalties.
4. Pool sends ETH refund to attacker's `receive()`, which calls `_setTokenRoyalty(tokenId, attacker, 10000)`.
5. Second loop re-reads _getRoyalty, now 100%, and pool transfers full salePrice to attacker as royalty.
6. Attacker keeps the NFT having paid ~0 to the pool, then sells it back to the same pool for base tokens.
**How found:** Code reading of buy() noticed _getRoyalty invoked twice with an untrusted ETH refund external call in between, enabling state change (royalty) between the two reads.
**Exploitation:** Malicious royalty receiver contract buys an NFT from the private pool, uses the ETH refund callback to raise its own royalty to 100%, causing the pool to pay the full sale price to the attacker while the attacker pays nothing to the pool; NFT is then sold back to drain base tokens.
**Escalation:** Repeat with each NFT / full pool balance drained in a single transaction.
**Insight:** When a fee/price is read once to compute totals and again to execute transfers, look for any external call between the reads that lets the counterparty mutate the value (ERC-2981 royalty setters, oracle updates, rebasing balances).
