Reproduced Exploit
DIA — unstake pays the live rate for days that accrued at the old rate
getRewardForStakingStore multiplies passedDays by whatever rewardRatePerDay is now. It does not remember the rate that was in force between stakingStartTime and the unstake request.
Chain
Other
Category
logic
Date
Jun 2025
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: 57921-incorrect-reward-calculation-when-reward-rate-changes-mixbyt. Standalone Foundry PoC and full write-up: 57921-incorrect-reward-calculation-when-reward-rate-changes-mixbyt_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/reward-calculation
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/57921-incorrect-reward-calculation-when-reward-rate-changes-mixbyt_exp.sol.
Key info#
| Impact | HIGH — a 10-day window that earned 1,000 is paid 10,000 WDIA |
| Protocol | DIA DIAWhitelistedStaking |
| Vulnerable code | (rewardRatePerDay * passedDays * principal) / 10000 |
| Bug class | The current rate is applied to the whole stored window |
| Finding | MixBytes — DIA Lumina Staking, June 2025 · id 57921 |
| Report | Lumina Staking |
| Source | AuditVault finding |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
getRewardForStakingStore multiplies passedDays by whatever rewardRatePerDay is now. It does not remember the rate that was in force between stakingStartTime and the unstake request.
This stake's stored window is 10 days. That window accrued at 100 bps, which is 1,000 WDIA on a 10,000 principal. The live rate is already 1,000. The quote is (1000 * 10 * 10000 ether) / 10000 = 10000 ether. Unstake pays the whole 10,000 and empties a pool that was funded for the larger number. The overpayment is 9,000 WDIA.
The 10 days are stored timestamps. run() does not warp. The rate change is prior state: the live rate is already the new one when the user unstakes.
The vulnerable code#
uint256 passedDays = passedSeconds / (24 * 60 * 60);
// @> VULN: (rewardRatePerDay * passedDays * principal) / 10000 uses the live rate
return (rewardRatePerDay * passedDays * currentStore.principal) / 10000;
Two identical stakes unstaked on either side of a rate change receive different amounts. The report's fix is an accumulator updated before the rate changes.
Root cause#
The formula has one rate and one window. After updateRewardRatePerDay the old rate is gone, so every day in the window is repriced.
Preconditions#
- Principal is 10,000 and the stored window is 10 days.
- The rate during that window was 100. The live rate is 1,000.
- The reward pool holds 10,000 WDIA.
Attack walkthrough#
- Open the 10-day stake.
- Quote at the live rate. The quote is 10,000.
- Unstake. The pool pays 10,000.
- The same window at rate 100 is 1,000. The difference is 9,000 taken from the pool.
Control: the quote multiplies by the stored old rate. Unstake pays 1,000 and 9,000 stays in the contract.
Diagrams#
Impact#
The staker receives 10,000 WDIA for a window that earned 1,000. The extra 9,000 is reward inventory that belonged to the rate actually in force. A later decrease of the rate underpays the same window by the same mechanism.
Remediation#
Keep an accumulator of reward per unit of stake and update it before rewardRatePerDay changes. Pay principal * (globalIndex - userIndex).
Sources#
- AuditVault finding 57921
- MixBytes DIA Lumina Staking, June 2025
- DIAWhitelistedStaking.sol at 50b70919
Taxonomy#
vuln/logic/reward-calculation·vuln/logic/incorrect-calculation
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 57921-incorrect-reward-calculation-when-reward-rate-changes-mixbyt_exp (evm-hack-registry mirror).
- AuditVault finding: 57921-incorrect-reward-calculation-when-reward-rate-changes-mixbyt.
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.