# Reentrancy in Vesting.sol:claim() will allow users to drain the contract due to executing .call() on user's address before setting s.index = uint128(i)

Vesting.claim() sends native BNB via .call() before updating s.index, letting a contract reenter and repeatedly claim until the contract is drained.

- Payout: Contest reward
- Severity: high
- Program: Zap Protocol
- Credited: 0x4non
- Disclosed: 2024-03-11
- Write-up quality: 6/10
- Original: https://github.com/sherlock-audit/2024-03-zap-protocol-judging/issues/157
- Library page: https://bugbountylibrary.com/r/sherlock%3A2024-03-zap-protocol%3AH-3
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:source-review vuln:reentrancy

**Target:** Vesting.sol claim() vesting contract
**Root cause:** claim() performs an external .call() to the caller to send native BNB before writing s.index = uint128(i), violating checks-effects-interactions so the 'already claimed' guard remains unset during the callback.
**Steps:**
1. Attacker deploys a contract with a deposit in Vesting and a receive() that calls claim() again.
2. Call claim(); 'already claimed' check passes since s.index is unset.
3. Contract computes pctAmount and executes payable(sender).call{value: pctAmount}("") before updating s.index.
4. In receive(), reenter claim(); s.index still unset so the guard passes again.
5. Repeat until the Vesting contract's balance is drained.
**How found:** Manual code review of claim(): noticed the state update s.index = uint128(i) occurs after the external .call() in the native-token branch, breaking CEI and leaving the claim guard stale during the callback.
**Exploitation:** Attacker contract with a vested deposit calls claim(); the native-token branch sends BNB via .call() to the attacker before s.index is updated, and the attacker's receive() reenters claim() each time, extracting pctAmount repeatedly until funds are exhausted.
**Insight:** In vesting/claim contracts, check whether the per-user claim cursor (index/amountClaimed) is written after any native-token .call(); a receive() that re-calls claim() will re-pass the 'already claimed' check because the cursor is still stale.
