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(…

Aug 2024Otheraccess-control3 min read

Chain

Other

Category

access-control

Date

Aug 2024

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

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.

Loading fork state…

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-registry mirror.


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#

ImpactHIGH — user rewards wiped; incentive tokens stuck in vault
ProtocolRoyco — ERC4626i incentivized vault
Vulnerable codeERC4626i.updateUserRewards — public overwrite of accumulated
Bug classPermissionless accrual overwrite; same-block second call zeros rewards
FindingCantina — Royco, Aug 2024 · #46672 · reporter Kurt Barry
Reportcantina_royco_august2024.pdf
SourceAuditVault
StatusAcknowledged — ERC4626i being rewritten
Compiler^0.8.24 (PoC)

TL;DR#

  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() always calls updateUserRewards first, so a poke + claim pays zero; reward tokens stay stuck.

The vulnerable code#

SOLIDITY
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 updateUserRewards for that user.
  • A subsequent same-block interaction (e.g. claim) re-runs the update.

Attack walkthrough#

  1. User deposits and opts into a rewards campaign (lastUpdate = 0).
  2. Attacker calls updateUserRewards → non-zero accumulated, lastUpdate = now.
  3. User (or attacker-triggered path) calls claim → second update with elapsed = 0 → wipe → claim 0.
  4. HARM: rewards erased; incentive tokens remain locked in the vault.

Diagrams#

sequenceDiagram participant Attacker participant Vault as ERC4626i participant User User->>Vault: deposit + optIntoCampaign Attacker->>Vault: updateUserRewards campaignId user Note over Vault: accumulated = earned, lastUpdate = now User->>Vault: claim Vault->>Vault: updateUserRewards again elapsed = 0 Note over Vault: accumulated overwritten to 0 Vault-->>User: pays 0 Note over Vault: incentive tokens stuck

Impact#

Any account can erase any user's unclaimed rewards. Tokens corresponding to wiped rewards cannot be recovered by users.

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

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.