# Lack of access control in AgentNftV2::addValidator() enables unauthorized validator injection and causes reward accounting inconsistencies

AgentNftV2.addValidator lacks access control; attacker pre-adds a validator for the predictable next virtualId, duplicating it at mint and overstating validatorPoolRewards, breaking reward claims.

- Payout: Contest reward
- Severity: high
- Program: Virtuals Protocol
- Credited: joicygiore
- Disclosed: 2025-08-04
- Write-up quality: 7/10
- Original: https://code4rena.com/reports/2025-04-virtuals-protocol#h-01
- Library page: https://bugbountylibrary.com/r/c4%3A2025-04-virtuals-protocol%3AH-01
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:access-control vuln:accounting-error vuln:reward-distribution

**Target:** AgentNftV2 / AgentRewardV2 reward accounting
**Root cause:** addValidator() is public with no role check, and mint() calls _addValidator() again for the founder, so a pre-registered validator gets pushed twice into _validators[virtualId].
**Steps:**
1. Read AgentFactoryV2.executeApplication(): it calls IAgentNft(nft).nextVirtualId() then mint(virtualId,...), so the next id is predictable.
2. Attacker calls `AgentNftV2.addValidator(nextVirtualId, attacker)` before the application executes; isValidator is false so _addValidator pushes the address.
3. executeApplication runs mint(), which calls `_addValidator(virtualId, founder)`; if attacker==founder (or any duplicate), _validators[virtualId] now holds the entry twice.
4. AgentRewardV2._distributeValidatorRewards iterates validatorCount entries, processing the duplicate twice and adding `validatorRewards - participationReward` to validatorPoolRewards twice.
5. withdrawValidatorPoolRewards() transfers the inflated pool, leaving the contract short and causing valid reward claims to fail.
**How found:** Code review of mint()/addValidator() visibility: noticed addValidator is public with no role while mint is MINTER_ROLE-gated, and that executeApplication exposes nextVirtualId() making the target id predictable; traced duplicate push into reward loop.
**Exploitation:** Attacker front-runs executeApplication by reading nextVirtualId(), calls addValidator to inject/duplicate a validator, then waits for reward distributions to inflate validatorPoolRewards; governance withdrawal drains the excess, underfunding legitimate validator rewards.
**Insight:** When a mint/init path calls an internal _add function that a public wrapper also exposes, check whether the public wrapper can pre-seed state for a predictable id (nextId getters) so the init path double-inserts into arrays used by reward loops.
