# CAROLProtocol: reentrancy price manipulation in sell() drains funds

CAROLProtocol's sell() reentrancy via ETH receive() let attacker manipulate ethReserved price, draining ~$53k via stacked flash loans.

- Payout: Real-world hack
- Severity: high
- Program: CAROLProtocol
- Analysis by: DeFiHackLabs
- Disclosed: 2023-11-30
- Write-up quality: 6/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2023-11/CAROLProtocol_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20231130-carolprotocol
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained vuln:flash-loan vuln:oracle-manipulation vuln:reentrancy

**Target:** Solidity staking/bonding contract on Base
**Root cause:** sell() calls swapExactTokensForETH which triggers the attacker's receive() before state (ethReserved) is updated, allowing price manipulation during reentrancy.
**Steps:**
1. Flash loan WETH from Synapse, Balancer, Kokonut, UniswapV3, and UniswapV2 (~3400 WETH total)
2. Swap WETH to CAROL via UniswapV2 router
3. Call `CAROLProtocol.sell(sellAmount)` in a loop; sell() invokes swapExactTokensForETH which calls attacker's `receive()`
4. In `receive()`, swap 90% of CAROL to WETH to manipulate ethReserved price
5. Repeat until sell succeeds, then convert CAROL back to WETH and repay all flash loans
**How found:** Attacker likely spotted that sell() performs an external swap (swapExactTokensForETH) before updating ethReserved, enabling reentrancy to manipulate the price used for redemption.
**Exploitation:** Stacked flash loans across five protocols to accumulate ~3400 WETH, swapped into CAROL, then repeatedly invoked sell() whose internal swap triggered attacker receive() reentrancy to skew ethReserved, extracting value before repaying loans.
**Escalation:** Chained five flash loan sources (Synapse, Balancer, Kokonut, UniV3, UniV2) to maximize capital for the price manipulation.
**Insight:** In bonding-curve/staking contracts, check whether sell/withdraw paths update the reserve/price state before or after the external swap call; a receive() hook during the swap lets you re-enter and skew the reserve used for payout.
