# Malicious delegatees can block delegators from redelegating and from sending their NFTs

Delegating to address(0) repeatedly underflows a delegatee's vote checkpoints, permanently blocking delegators from redelegating or transferring their NFTs.

- Payout: Contest reward
- Severity: high
- Program: Collective
- Credited: bart1e
- Disclosed: 2024-02-08
- Write-up quality: 8/10
- Original: https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/49
- Library page: https://bugbountylibrary.com/r/c4%3A2023-12-revolutionprotocol%3AH-04
- Tags: domain:web3 impact:dos impact:integrity kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:incomplete-fix technique:source-review vuln:access-control vuln:business-logic

**Target:** VotesUpgradeable / ERC721CheckpointableUpgradeable voting delegation
**Root cause:** delegates() returns account when _delegatee is address(0), so _delegate calls _moveDelegateVotes(account, 0, units) repeatedly, subtracting votes from the account's checkpoint each time until it underflows.
**Steps:**
1. Mint NFTs to victim (2) and attacker (1)
2. Victim calls `erc721Token.delegate(attacker)`
3. Attacker calls `erc721Token.delegate(address(0))` twice (or units times)
4. Victim calls `erc721Token.delegate(user)` -> reverts on underflow in `_moveDelegateVotes`
5. Victim calls `erc721Token.transferFrom(user, addr, 0)` -> reverts
**How found:** Code-reading of the modified delegates() convenience override against OpenZeppelin's original, noticing that _moveDelegateVotes can be invoked repeatedly with to==0, which the original implementation prevented via the from==to guard.
**Exploitation:** Attacker receives delegation from victim, then repeatedly delegates to address(0) to drain the attacker's own vote checkpoint to zero; any subsequent transfer or redelegation by the victim tries to subtract votes from the now-empty checkpoint and reverts, freezing victim's NFTs. Attacker can then move their own NFT away at no cost.
**Insight:** When a fork overrides a library's default-return semantics (e.g. delegates() returning account instead of address(0)), re-audit every caller that relied on the original sentinel value; here the from==to guard in _moveDelegateVotes silently stopped protecting against repeated zero-address operations.
