# Relocking through MFDBase::claimBounty() leads to accounting error and causes funds loss

claimBounty() with auto-relock unstakes user funds from the gauge but never re-stakes them, so later withdrawExpiredLocks() reverts on underflow and funds are stuck.

- Payout: Contest reward
- Severity: high
- Program: Defi App
- Credited: KlosMitSoss
- Disclosed: 2025-03-27
- Write-up quality: 7/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_defi_app_february2025.pdf#3.1.3
- Library page: https://bugbountylibrary.com/r/cantina%3Adefi_app_february2025%3A3.1.3
- Tags: domain:web3 impact:dos impact:funds-loss kind:contest platform:cantina sev:high tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** Solidity staking contract (MFDBase/DefiAppStaker) with gauge integration
**Root cause:** claimBounty() unconditionally calls _beforeWithdrawExpiredLocks(), which withdraws from the gauge, even when handleWithdrawOrRelockLogic() relocks via stakeLogic(); the relock path skips _afterStakeHook() so the gauge is never re-deposited, desyncing gauge balance from userBalances.
**Steps:**
1. User stakes via lockzap; _afterStakeHook deposits into gauge, gauge.balanceOf(staker) == lpAmount
2. User enables auto-relock: `staker.setAutoRelock(true)`
3. BountyManager calls `staker.claimBounty(User1.addr, true)`
4. claimBounty calls _beforeWithdrawExpiredLocks -> `gauge.withdraw(amount)`, then relocks via stakeLogic without re-depositing to gauge
5. Assert `gauge.balanceOf(staker) == 0` while userBalances.locked still equals lpAmount
6. After lock expiry, `staker.withdrawExpiredLocks()` reverts with arithmetic underflow
**How found:** Code-reading of the stake/withdraw hook symmetry: noticed _afterStakeHook() deposits to gauge and _beforeWithdrawExpiredLocks() withdraws, then observed claimBounty() invokes the withdraw hook even on the relock branch that calls stakeLogic() without the after-stake hook.
**Exploitation:** Precondition: victim has auto-relock enabled and a bounty claim is executed against them. The claim drains the gauge position without re-staking, leaving user accounting intact but gauge empty; the user's subsequent withdrawExpiredLocks() underflows and reverts, permanently locking the staked funds.
**Insight:** When a protocol has paired before/after hooks around stake and withdraw, audit every alternate entry point (claim, relock, migrate) for asymmetric hook invocation — a relock that reuses stakeLogic() bypasses _afterStakeHook and silently desyncs external gauge accounting from internal balances.
