# onBalanceChange causes previously unclaimed rewards to be cleared

FeeSplitter.onBalanceChange resets userFeeOffset without crediting accrued fees, so a user's unclaimed rewards are wiped on any balance change like a second token purchase.

- Payout: Contest reward
- Severity: medium
- Program: Curves Protocol
- Credited: kutugu
- Disclosed: 2024-07-19
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2024-01-curves-findings/issues/39
- Library page: https://bugbountylibrary.com/r/c4%3A2024-01-curves%3AM-10
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:source-review vuln:business-logic vuln:reward-distribution

**Target:** FeeSplitter.sol fee redistribution contract (Solidity/EVM)
**Root cause:** onBalanceChange sets data.userFeeOffset[account] = data.cumulativeFeePerToken directly instead of calling updateFeeCredit, so the owed amount between the old offset and current cumulativeFeePerToken is never added to unclaimedFees before the offset advances.
**Steps:**
1. Deploy Curves and FeeSplitter; user buys 1 token of a curves token via `buyCurvesTokenWithName`.
2. Attacker calls `buyCurvesToken{value: getBuyPriceAfterFee(token,1)}(token,1)`; `getClaimableFees` shows accrued fees (e.g. 3125000000000).
3. Attacker buys the same token again; onBalanceChange fires and resets userFeeOffset, so `getClaimableFees` no longer includes the first purchase's fees (only 16666666666666 instead of sum).
4. Compare with claiming via `claimFees` before the second buy to see the lost amount.
**How found:** Code review of FeeSplitter's accounting: noticed onBalanceChange overwrote userFeeOffset without first settling owed fees, unlike updateFeeCredit; confirmed with Foundry PoC comparing claimable fees across two purchases.
**Exploitation:** Any balance-changing action (buy/sell/transfer) triggers onBalanceChange, silently zeroing the account's pending fee credit; repeated actions cause cumulative loss of earned rewards to the user.
**Insight:** When an accounting function advances a cumulative-index offset, verify every path that writes the offset first credits (offset_delta * balance) to the pending bucket; here a manager hook bypassed the shared updateFeeCredit helper, so grep for direct offset assignments outside the settle function.
