# Critical access control flaw: Role removal logic incorrectly grants unauthorized roles

removeRole() in PermissionsRegistry swaps the removed role with the last entry of the global roles array, silently granting users an unrelated admin role.

- Payout: Contest reward
- Severity: medium
- Program: Blackhole
- Credited: rayss
- Disclosed: 2025-07-03
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2025-05-blackhole#m-02
- Library page: https://bugbountylibrary.com/r/c4%3A2025-05-blackhole%3AM-02
- Tags: domain:web3 impact:privesc kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:access-control vuln:privilege-escalation

**Target:** PermissionsRegistry.sol role management contract
**Root cause:** When removing a role from a user's _addressToRoles array, the code replaces the removed slot with _roles[_roles.length-1] (the global roles array's last element) instead of the user's own last role, injecting an unrelated role.
**Steps:**
1. Global roles: `["GOVERNANCE","VOTER_ADMIN","GAUGE_ADMIN","BRIBE_ADMIN"]`
2. Alice roles: `["VOTER_ADMIN","GAUGE_ADMIN"]`
3. Call `removeRole("GAUGE_ADMIN")`
4. Code executes `_addressToRoles[Alice][1] = _roles[_roles.length-1]` → `"BRIBE_ADMIN"`, then `pop()`
5. Alice now holds `["VOTER_ADMIN","BRIBE_ADMIN"]` without authorization
**How found:** Code review of removeRole() array-compaction logic; noticed the swap referenced the global _roles array rather than the user's own array.
**Exploitation:** Any user with a removable role triggers the bug on role removal; the last global role (e.g. BRIBE_ADMIN/GAUGE_ADMIN) is silently assigned, granting admin privileges over gauges/bribes.
**Insight:** When auditing array-removal 'swap-and-pop' patterns, verify the swap source is the same array being popped — copy-paste from a global list into a per-user list is a classic role-corruption bug.
