# Delegations cannot be removed in some cases due to vulnerable revokeDelegate() implementation

Gondi's revokeDelegate always passes empty rights to DelegateRegistry, so delegations made with custom rights can never be revoked, letting an old borrower keep claiming on a new borrower's NFT.

- Payout: Audit contest
- Severity: medium
- Program: Gondi
- Found by: oakcobalt
- Disclosed: 2024-12-17
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2024-06-gondi#m-01
- Library page: https://bugbountylibrary.com/r/c4%3A2024-06-gondi%3AM-01
- Tags: domain:web3 impact:integrity kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic

**Target:** MultiSourceLoan::revokeDelegate / DelegateRegistry delegateERC721
**Root cause:** revokeDelegate hardcodes empty bytes32 rights when calling delegateERC721, but DelegateRegistry keys delegations by hashed rights, so a delegation created with custom rights is stored under a different key and never cleared.
**Steps:**
1. Borrower calls `delegate(_loanId, loan, _delegate, customRights, true)` which stores delegation under key hash(rights).
2. Loan ends, NFT transfers to a new borrower.
3. Protocol/new borrower calls `revokeDelegate(_delegate, _collection, _tokenId)` which passes `""` rights.
4. DelegateRegistry reads a different key, no-op; old delegation persists.
5. New borrower re-delegates to hot wallet; old borrower still claims via stale delegation.
**How found:** Code reading: noticed delegate() accepts arbitrary _rights while revokeDelegate() hardcodes empty rights, and DelegateRegistry hashes rights into the storage key, making revocation a no-op for custom-rights delegations.
**Exploitation:** Old borrower retains a valid delegation on the collateral NFT after transfer; when the new borrower delegates for airdrops/tickets, the old borrower's stale delegation still resolves and can claim the new borrower's entitlements.
**Insight:** When integrating external registries that key state by a hashed parameter (rights/roles/salt), check that every revoke/cleanup path passes the identical parameter; hardcoded defaults silently miss entries.
