# Bitmap shift and mask width bugs

Shifts at or beyond bit width silently return zero; masks with wrong width clear adjacent packed fields

_Area: DeFi accounting and math bugs. Seen in 4 disclosed reports; latest 2026-03-09. Updated 2026-10-23._

## How it works
A bitmap or packed struct encodes many fields in one word. Two assumptions break silently: (1) that a shift amount is always smaller than the word width, and (2) that a mask constant covers exactly the intended bits. In EVM/Solidity, shifting by >= 256 yields 0 rather than reverting, and an AND with a mask of the wrong width clears neighboring fields. Neither produces an error; the code just reads or writes the wrong bits, so a guard that should block an action never fires, or a field is silently zeroed.

## Where it shows up
- Claimed-set bitmaps indexed by an unbounded integer (epoch IDs, round IDs, token IDs) [E2](https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md)
- `(bitmap >> index) & 1` checks where `index` type is wider than the bitmap [E2](https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md)
- Packed-struct encode/decode in assembly with hand-written shifts and offsets [E1](https://bugbountylibrary.com/r/c4%3A2022-10-traderjoe%3AH-03.md)
- Hex mask constants (`UPPER_*BITS_MASK`) whose length does not match the struct layout [E3](https://bugbountylibrary.com/r/c4%3A2025-12-panoptic-next-core%3AM-04.md)
- Comments asserting an index is "small" or "bounded" without an enforced check [E2](https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md)

## How to review
The invariant: every shift amount is provably < bit width, and every mask covers exactly the intended bit range. Concretely:
- For each bitmap index, find the bound. If the index type is uint256 and the bitmap is uint256, require an explicit `require(id < 256)` or equivalent before the shift [E2](https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md).
- For each mask constant, recompute the bit width from the struct field layout and compare to the literal's hex length [E3](https://bugbountylibrary.com/r/c4%3A2025-12-panoptic-next-core%3AM-04.md).
- For each packed encode/decode, verify every shift offset matches the field's position; a missing `shl` corrupts low fields instead of reverting [E1](https://bugbountylibrary.com/r/c4%3A2022-10-traderjoe%3AH-03.md).
- Check that a wrong result would be caught: does any downstream assertion or balance check detect the corruption?

## Variants seen
- Unbounded index into a fixed-width claimed bitmap, disabling the double-claim guard [E2](https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md)
- Missing left-shift when packing fee parameters, overwriting binStep and zeroing high fields [E1](https://bugbountylibrary.com/r/c4%3A2022-10-traderjoe%3AH-03.md)
- Mask one field-width too narrow, clearing EMAs and lockMode during rebase [E3](https://bugbountylibrary.com/r/c4%3A2025-12-panoptic-next-core%3AM-04.md)

## Impact and severity
High: repeated reward claims drain funds [E2](https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md); corrupted packed state bricks swaps/mints and locks user tokens [E1](https://bugbountylibrary.com/r/c4%3A2022-10-traderjoe%3AH-03.md); silent state corruption in oracle packs [E3](https://bugbountylibrary.com/r/c4%3A2025-12-panoptic-next-core%3AM-04.md). Severity rises when the corrupted value feeds accounting (balances, shares, rewards) and no invariant check catches the drift.

## Why fixes fail
Evidence shows the pattern recurring across unrelated protocols (rewards, AMM fees, oracle packing), suggesting fixes address one site without auditing other shifts/masks in the same codebase.

## Evidence
- E1: https://bugbountylibrary.com/r/c4%3A2022-10-traderjoe%3AH-03.md
- E2: https://bugbountylibrary.com/r/c4%3A2021-12-pooltogether%3AH-06.md
- E3: https://bugbountylibrary.com/r/c4%3A2025-12-panoptic-next-core%3AM-04.md
