# userTotalStaked invariant will be broken due to vulnerable implementations in release()

IdentityStaking.release() restores selfStakes/communityStakes amounts but not userTotalStaked, so withdraw underflows and users lose slashed-then-released funds.

- Payout: Contest reward
- Severity: high
- Program: Gitcoin Passport
- Credited: oakcobalt
- Disclosed: 2024-04-19
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2024-03-gitcoin-findings/issues/9
- Library page: https://bugbountylibrary.com/r/c4%3A2024-03-gitcoin%3AH-01
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:business-logic vuln:dos

**Target:** IdentityStaking.sol staking contract release() flow
**Root cause:** release() increments selfStakes[staker].amount / communityStakes[staker][stakee].amount without updating userTotalStaked, breaking the documented invariant that userTotalStaked equals the sum of stake amounts.
**Steps:**
1. Alice selfStakes 100000 and communityStakes 100000 at round 1.
2. Owner calls `slash([alice],[alice],[stakee],80)` — 80% slashed, amounts drop to 20000 each, totalSlashed(1)=160000.
3. Owner calls `release(alice, alice, 80000, 1)` and `release(alice, stakee, 80000, 1)` — stake amounts restored to 100000 each but userTotalStaked stays at 40000.
4. Advance time past lock (twelveWeeksInSeconds+1).
5. Alice calls `withdrawSelfStake(100000)` and `withdrawCommunityStake(stakee,100000)` — both revert with arithmetic underflow panic.
6. Alice can only withdraw 20000 each; 80000x2 remains locked.
**How found:** Read the readme's stated invariant `userTotalStaked = selfStakes + sum(communityStakes)` and compared every state-mutating flow (stake/withdraw/slash) against release(), spotting that release() updates stake amounts but omits userTotalStaked.
**Exploitation:** No attacker needed; the accounting bug triggers on the legitimate slash-then-release path, permanently locking the released portion of a user's stake because withdraw subtracts from the stale userTotalStaked.
**Insight:** When a protocol documents an accounting invariant in its readme, diff every state-mutating function against it — the odd-one-out function (here release(), the least-exercised path) is where the invariant silently breaks and later underflows.
