# Stale internal balance tracker drift

Cached balance or timestamp read where a live balance or refresh is required after external movement

_Area: DeFi accounting and math bugs. Seen in 3 disclosed reports; latest 2025-07-15. Updated 2026-11-01._

## How it works
A contract keeps its own copy of a quantity that also lives somewhere else: a staked amount, a free/locked split, or a per-record timestamp. The copy is only updated on some paths. Anything that moves the real quantity outside those paths — reward accrual, slashing, a redemption, an early `continue` — leaves the copy stale. Later logic trusts the copy, so it computes rewards, collateral, or delegation amounts from a number that no longer matches reality. The broken assumption is "the tracker equals the truth at read time"; it only equals the truth right after the last write.

## Where it shows up
- Accumulators that use a per-record timestamp (`lastUpdated`, `lastWithdrawRewards`) but only refresh it inside some branches.
- `continue`/early-return paths between reading a timestamp and writing it back.
- Paired fields (locked vs free, rawLocked vs freeCollateral, debt vs earmarked) where one is mutated and the other is not.
- Functions that read a cached `pool.amount`/`totalStaked` instead of querying the source-of-truth module.
- Redeem, unstake, redelegate, or withdraw paths that move a whole position.

## How to review
For every cached field, list all writers and all readers. The invariant: at each read, the cached value must equal the live source-of-truth value, or the code must reconcile before use. Check:
- Does every path that changes the real balance also update the cache, including early exits?
- Are paired fields always adjusted together (decrement one, increment the other)?
- Does any operation that moves the full position query the authoritative module rather than the cache?
- For timestamp accumulators: is the timestamp written on every path that accumulates against it?

## How finders spotted it
- Read `updateLocked()` and saw the accumulator uses `vouch.lastUpdated`, but the refresh sits after a `lockAmount==0` continue, so maxed vouches never refresh [E1](https://bugbountylibrary.com/r/sherlock%3A2023-02-union%3AH-2.md).
- Diffed `_sync()`: `rawLocked` decremented, `freeCollateral` not incremented; confirmed with a Foundry test that withdraw reverts [E2](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.1.18.md).
- Noticed `redelegate_lp` reads internal `pool.amount` (updated only by add_stake/unstake/unlock_lp) instead of querying the mstaking module, then reasoned about reward/slashing drift [E3](https://bugbountylibrary.com/r/c4%3A2025-04-cabal-liquid-staking-token%3AM-06.md).

## Variants seen
- Timestamp not refreshed on a saturated record, inflating an accumulator [E1](https://bugbountylibrary.com/r/sherlock%3A2023-02-union%3AH-2.md).
- Paired balance field not incremented on redemption, stranding collateral [E2](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.1.18.md).
- Cached staked amount drifting from real delegation via rewards/slashing, causing failed redelegation or orphaned funds [E3](https://bugbountylibrary.com/r/c4%3A2025-04-cabal-liquid-staking-token%3AM-06.md).

## Impact and severity
E1 let a staker inflate `lockedCoinAge` and drain all UNION rewards, scaling with blocks waited between borrows [E1](https://bugbountylibrary.com/r/sherlock%3A2023-02-union%3AH-2.md). E2 permanently locked users' leftover collateral with no sweep [E2](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.1.18.md). E3 caused redelegation failures or orphaned funds after slashing or reward accrual [E3](https://bugbountylibrary.com/r/c4%3A2025-04-cabal-liquid-staking-token%3AM-06.md). Severity rises with how much value the stale number gates and whether drift is attacker-triggerable.

## Evidence
- E1: https://bugbountylibrary.com/r/sherlock%3A2023-02-union%3AH-2.md
- E2: https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.1.18.md
- E3: https://bugbountylibrary.com/r/c4%3A2025-04-cabal-liquid-staking-token%3AM-06.md
