# `cancelVesting` will potentially not give users unclaimed, vested funds, even if giveUnclaimed = true

cancelVesting mutates a memory copy of UserVesting, so locked stays nonzero and claimable() returns zero, denying users their vested funds.

- Payout: Contest reward
- Severity: high
- Program: Truflation
- Credited: s1ce
- Disclosed: 2023-12-22
- Write-up quality: 6/10
- Original: https://github.com/sherlock-audit/2023-12-truflation-judging/issues/192
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-12-truflation%3AH-2
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** TrufVesting.sol cancelVesting vesting contract
**Root cause:** userVesting is loaded as memory, so setting userVesting.locked = 0 after unstaking does not persist to storage; claimable() reads the unaltered storage struct and still treats funds as locked.
**Steps:**
1. User stakes all vested funds via veTRUF, setting lockupIds and userVesting.locked.
2. Owner calls `cancelVesting(categoryId, vestingId, user, true)`.
3. Contract calls `veTRUF.unstakeVesting(user, lockupId - 1, true)` and sets `userVesting.locked = 0` on the memory copy.
4. `claimable(categoryId, vestingId, user)` reads storage where locked is still nonzero, returns 0.
5. giveUnclaimed branch skipped; user receives no vested funds.
**How found:** Code reading of cancelVesting: noticed the explicit `userVesting.locked = 0` line exists to make unstaked funds claimable, then observed the struct is declared `memory` so the write is lost before claimable() is called.
**Insight:** When a function deliberately zeroes a field before calling a view helper, verify the struct is `storage`; a `memory` copy silently defeats the intent and the helper reads stale state.
