# Adversary can reenter takeOverDebt() during liquidation to steal vault funds

Missing nonReentrant on takeOverDebt() lets an attacker reenter during repay()'s UniV3 swap, duplicating a liquidated position and double-refunding LPs to drain the vault.

- Payout: Contest reward
- Severity: high
- Program: Real Wagmi
- Credited: 0x52
- Disclosed: 2023-10-07
- Write-up quality: 8/10
- Original: https://github.com/sherlock-audit/2023-10-real-wagmi-judging/issues/76
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-10-real-wagmi%3AH-2
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:chained vuln:business-logic vuln:reentrancy

**Target:** LiquidityBorrowingManager / LiquidityManager vault lending contract
**Root cause:** takeOverDebt() lacks a nonReentrant modifier and borrowing storage is only cleared after the UniV3 swap control transfer in repay(), so a liquidatable position can be both liquidated and taken over simultaneously.
**Steps:**
1. Deploy a custom token whose transfer() reenters the protocol.
2. Fund a UniV3 LP with target token + custom token.
3. Borrow against the LP with target token as hold token.
4. Wait until position is liquidatable, call repay() to liquidate.
5. During the `_v3SwapExactInput` swap, custom token transfer hands control to attacker.
6. Reenter `takeOverDebt()` on the still-intact borrowing state, moving loan to a second address.
7. Resume repay(); `_removeKeysAndClearStorage` runs on the now-deleted key (removeKey doesn't revert on missing key), refunding all initial LP.
8. Position still exists on new address; later liquidate it again for more LP.
9. Vault refunds LP twice with no new backing, creating deficit; repeat to drain.
**How found:** Code review noting takeOverDebt() lacked nonReentrant and that repay() performs an external UniV3 swap before clearing borrowing storage, plus observing removeKey() silently no-ops on missing keys so repay() completes after the key was consumed.
**Exploitation:** Attacker borrows against own LP, triggers liquidation via repay(), reenters takeOverDebt() mid-swap to transfer the loan to a fresh address while repay() still refunds the original LP from the vault. The duplicated position is later liquidated again, extracting a second refund from the vault with no new collateral added.
**Escalation:** Loop the borrow/liquidate/reenter cycle to repeatedly double-dip and drain the vault of the hold token.
**Insight:** When auditing liquidation/repay flows, check whether state is cleared only after external swap calls and whether sibling functions like takeOverDebt lack the reentrancy guard; also note array-remove helpers that don't revert on missing keys, which let a stale outer call finish cleanly after reentrancy.
