# Joe Agent: reentrancy in removeLiquidityViaContract drains LP pool

Joe Agent's removeLiquidityViaContract sent BNB before zeroing lpInfo, letting a receive() re-enter 25x and drain 62.5 BNB plus JOE from the shared pool.

- Payout: Real-world hack
- Severity: high
- Program: Joe Agent
- Credited: DeFiHackLabs
- Disclosed: 2026-05-27
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-05/JoeAgent_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260527-joeagent
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:source-review vuln:business-logic vuln:reentrancy

**Target:** BEP-20 token contract (ERC1967 proxy) with LP custody and PancakeSwap withdrawal
**Root cause:** removeLiquidityViaContract forwards unwrapped BNB to the user via low-level call before decrementing lpInfo[user].lpAmount, violating checks-effects-interactions. The stale lpAmount lets the recipient re-enter and pass the `liquidity <= lpInfo[user].lpAmount` check repeatedly.
**Steps:**
1. Attacker calls `zapNativeForLP{value: 5 ether}(referrer,0,0,0,deadline)` to park a single ~437 LP position, setting lpInfo[attacker].lpAmount.
2. Wait past any LP lock window (warp 1h).
3. Call `removeLiquidityViaContract(lpAmount,0,0,deadline)`; contract pulls LP from PancakeSwap, unwraps WBNB, sends BNB to attacker via low-level call.
4. In `receive()`, re-enter `removeLiquidityViaContract(lpAmount,...)` while lpInfo is still un-zeroed; repeat up to 25 nested calls.
5. Each re-entry burns a fresh slice of pool LP (not attacker's), yielding ~2.5 BNB per loop; sweep BNB and JOE to attacker EOA.
**Exploitation:** Seed one ~437 LP position with ~5 BNB via zapNativeForLP, then trigger removeLiquidityViaContract. The BNB payout arrives before lpInfo is zeroed, so the attacker's receive() re-enters with the same liquidity value 25 times. Each nested call passes the balance check and burns LP belonging to the whole depositor pool, extracting ~62.5 BNB plus ~1.196M JOE against a single paid position (~57.5 BNB net profit).
**Escalation:** Recursion depth (25 loops) multiplies a single position's withdrawal into 25x the pool's LP, scaling loss linearly with loop count.
**Insight:** When a token contract custodies user LP and pays out native/token before updating per-user accounting, the payout callback is a re-entry vector: check whether lpInfo/balances are decremented before or after the external call, and whether the balance check uses the stale value.
