Reproduced Exploit

DIA — paidOutReward makes the next unstake revert, and 10000 stays locked

A 10,000 stake that has already run 10 days at 100 bps has a reward of 1,000. The first unstake pays that 1,000, sets reward to 0, and leaves paidOutReward at 1,000.

Jun 2025•Other•arithmetic•3 min read

Chain

Other

Category

arithmetic

Date

Jun 2025

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: 57913-paidoutreward-subtraction-may-lead-to-unstake-and-unstakepri. Standalone Foundry PoC and full write-up: 57913-paidoutreward-subtraction-may-lead-to-unstake-and-unstakepri_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/arithmetic/underflow · vuln/dos/frozen-funds

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/57913-paidoutreward-subtraction-may-lead-to-unstake-and-unstakepri_exp.sol.


Key info#

ImpactHIGH — the second unstake reverts and 10,000 WDIA of principal stays locked
ProtocolDIA DIAWhitelistedStaking
Vulnerable coderewardToSend = currentStore.reward - currentStore.paidOutReward
Bug classReward is zeroed and paidOutReward is not
FindingMixBytes — DIA Lumina Staking, June 2025 · id 57913
ReportLumina Staking
SourceAuditVault finding
Compilersolc 0.8.27, cancun, via IR

TL;DR#

A 10,000 stake that has already run 10 days at 100 bps has a reward of 1,000. The first unstake pays that 1,000, sets reward to 0, and leaves paidOutReward at 1,000.

The user requests unstake again in the same block. The new window is 0 days, so the next reward is 0. unstakePrincipal computes 0 - 1000 and Solidity 0.8 reverts. The 10,000 principal never moves.

The 10 days are stored timestamps, 1 and 1 + 10 days. run() does not warp.

The vulnerable code#

SOLIDITY
updateReward(stakingStoreIndex);
// @> VULN: rewardToSend = currentStore.reward - currentStore.paidOutReward underflows
uint256 rewardToSend = currentStore.reward - currentStore.paidOutReward;

The first unstake is the line the report describes: it zeros currentStore.reward after adding the payout to paidOutReward, and it does not clear paidOutReward.

Root cause#

paidOutReward is a lifetime accumulator. reward is reset to 0 on every claim. The next short window cannot catch up to the accumulator, so the subtraction underflows and both unstake and unstakePrincipal revert.

Preconditions#

  • Rate is 100, meaning 1 percent per day.
  • The stored interval is 10 full days, so the first reward is 1,000.
  • The user claims once, then requests unstake again before another full day accrues.

Attack walkthrough#

  1. Put 11,000 WDIA on the staking contract and record a 10,000 stake.
  2. First unstake pays 1,000, zeros reward, and leaves paidOutReward at 1,000.
  3. requestUnstake sets the request time to this block. A fresh calculation is 0.
  4. unstakePrincipal reverts on 0 - 1000.
  5. The contract still holds 10,000 WDIA. The user holds only the first 1,000.

Control: the second call pays principal from getRewardForStakingStore and does not subtract the stale paidOutReward. The user receives the 10,000 and the contract balance is 0.

Diagrams#

flowchart TD A["Claim pays 1000 and zeros reward"] --> B["paidOutReward stays 1000"] B --> C["Next window is 0 days"] C --> D["0 minus 1000 reverts"] D --> E["10000 WDIA stays locked"]

Impact#

The staker cannot withdraw the remaining 10,000 WDIA. The report notes they also cannot wait for a larger reward: a second request is refused while the first request is still open, and fulfilling that request is what reverts.

Remediation#

Pay getRewardForStakingStore minus what was already paid only when the new total is greater. When reward is zeroed, do not keep a paidOutReward that the next zero reward cannot cover.

Sources#

Taxonomy#

  • vuln/arithmetic/underflow · vuln/dos/frozen-funds

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.