Reproduced Exploit
Royco ERC4626i — User rewards can be permissionlessly erased
1. updateUserRewards(campaignId, user) is public and callable for any user. 2. It overwrites userData.accumulated = (balance elapsed rate) / WAD instead of accruing. 3. After a first call sets rewards and lastUpdate = now, a second call in the same block has elapsed = 0 → accumulated = 0. 4. claim(…
Chain
Other
Category
access-control
Date
Aug 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 46672-user-rewards-can-be-permissionlessly-erased-cantina-none-roy. Standalone Foundry PoC and full write-up: 46672-user-rewards-can-be-permissionlessly-erased-cantina-none-roy_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/missing-modifier · reward-theft · reward-accounting
Reproduction: self-contained Foundry PoC, offline, forge-std only. Full trace: output.txt. PoC: test/46672-user-rewards-can-be-permissionlessly-erased-cantina-none-roy_exp.sol.
Key info#
| Impact | HIGH — user rewards wiped; incentive tokens stuck in vault |
| Protocol | Royco — ERC4626i incentivized vault |
| Vulnerable code | ERC4626i.updateUserRewards — public overwrite of accumulated |
| Bug class | Permissionless accrual overwrite; same-block second call zeros rewards |
| Finding | Cantina — Royco, Aug 2024 · #46672 · reporter Kurt Barry |
| Report | cantina_royco_august2024.pdf |
| Source | AuditVault |
| Status | Acknowledged — ERC4626i being rewritten |
| Compiler | ^0.8.24 (PoC) |
TL;DR#
updateUserRewards(campaignId, user)is public and callable for any user.- It overwrites
userData.accumulated = (balance * elapsed * rate) / WADinstead of accruing. - After a first call sets rewards and
lastUpdate = now, a second call in the same block haselapsed = 0→ accumulated = 0. claim()always callsupdateUserRewardsfirst, so a poke + claim pays zero; reward tokens stay stuck.
The vulnerable code#
userData.accumulated = (balanceOf[user] * elapsed * _campaignData.rate) / WAD; // @> VULN
userData.lastUpdate = block.timestamp;
Fix: use proper reward-index accumulation (+=); restrict updates to self / balance-changing hooks.
Root cause#
Overwrite-not-accrue math combined with a permissionless entrypoint and shared lastUpdate.
Preconditions#
- User opted into a campaign and holds shares.
- Attacker (or any contract) can call
updateUserRewardsfor that user. - A subsequent same-block interaction (e.g.
claim) re-runs the update.
Attack walkthrough#
- User deposits and opts into a rewards campaign (
lastUpdate = 0). - Attacker calls
updateUserRewards→ non-zeroaccumulated,lastUpdate = now. - User (or attacker-triggered path) calls
claim→ second update withelapsed = 0→ wipe → claim 0. - HARM: rewards erased; incentive tokens remain locked in the vault.
Diagrams#
Impact#
Any account can erase any user's unclaimed rewards. Tokens corresponding to wiped rewards cannot be recovered by users.
Sources#
- AuditVault finding #46672
- Cantina report — Royco (Aug 2024)
- Reduced C2 synthetic from finding-quoted
updateUserRewardsformula and PoC
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 46672-user-rewards-can-be-permissionlessly-erased-cantina-none-roy_exp (evm-hack-registry mirror).
- AuditVault finding: 46672-user-rewards-can-be-permissionlessly-erased-cantina-none-roy.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.