Reproduced Exploit

DIA — a 100 percent principal share cannot unstake, because WDIA rejects a zero transfer

A stake with principalWalletShareBps = 10000 assigns every reward to the principal wallet. Unstaking 1,100 (1,000 principal plus 100 reward) therefore sets requestedUnstakeRewardAmount to 0.

Jun 2025•Other•dos•3 min read

Chain

Other

Category

dos

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: 57917-wdia-prohibits-zero-transfers-which-will-block-rewards-distr. Standalone Foundry PoC and full write-up: 57917-wdia-prohibits-zero-transfers-which-will-block-rewards-distr_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/dos/frozen-funds · vuln/input-validation/boundary

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/57917-wdia-prohibits-zero-transfers-which-will-block-rewards-distr_exp.sol.


Key info#

ImpactHIGH — unstake reverts and 1,100 WDIA stays in the pool
ProtocolDIA WDIA and DIAExternalStaking
Vulnerable codeif (wad == 0) revert ZeroTransferAmount() on the beneficiary transfer
Bug classA zero beneficiary amount reverts the whole unstake
FindingMixBytes — DIA Lumina Staking, June 2025 · id 57917
ReportLumina Staking
SourceAuditVault finding
Compilersolc 0.8.27, cancun, via IR

TL;DR#

A stake with principalWalletShareBps = 10000 assigns every reward to the principal wallet. Unstaking 1,100 (1,000 principal plus 100 reward) therefore sets requestedUnstakeRewardAmount to 0.

unstake still calls WDIA.transfer for that 0. WDIA reverts with ZeroTransferAmount. The whole call rolls back. The request flag stays set, so the user cannot request again, and the next unstake hits the same zero transfer. 1,100 WDIA remains in the pool.

unstakingDuration is 0, so the request and the unstake are the same block. run() does not warp.

The vulnerable code#

SOLIDITY
function transferFrom(address src, address dst, uint256 wad) public returns (bool) {
    // @> VULN: if (wad == 0) revert ZeroTransferAmount()
    if (wad == 0) revert ZeroTransferAmount();
    ...
}

STAKING_TOKEN.transfer(currentStore.beneficiary, currentStore.requestedUnstakeRewardAmount);

The principal-wallet reward transfer is guarded with > 0. The beneficiary transfer is not. At a 100 percent principal share that amount is 0.

Root cause#

A 100 percent principal share is a configured, valid split. It produces a zero transfer, and the token treats zero as an error instead of a no-op. The staking contract does not skip it.

Preconditions#

  • principalWalletShareBps is 10,000.
  • The position has a non-zero reward, here 100 WDIA on 1,000 of principal, so the unstake amount is 1,100 and the beneficiary slice is 0.
  • WDIA reverts on wad == 0.

Attack walkthrough#

  1. Stake 1,000 at 10,000 bps and add 100 of rewards.
  2. Request unstake of 1,100. The beneficiary amount reads back as 0.
  3. unstake transfers the principal amounts and then transfers 0 to the beneficiary.
  4. WDIA reverts. The call returns false.
  5. The pool still holds 1,100. The user holds 0.

Control: skip any transfer whose amount is 0. The user receives the full 1,100 and the pool is empty.

Diagrams#

flowchart TD A["Share bps is 10000"] --> B["Beneficiary amount is 0"] B --> C["WDIA reverts on a zero wad"] C --> D["unstake rolls back"] D --> E["1100 WDIA stays in the pool"]

Impact#

The staker cannot finish the unstake. 1,100 WDIA is stuck: 1,000 of their principal and 100 of rewards. A second request is rejected because the first request was stored, and repeating unstake hits the same revert.

Remediation#

Do not call transfer when requestedUnstakeRewardAmount or the principal amount is 0. The token may also accept a zero wad.

Sources#

Taxonomy#

  • vuln/dos/frozen-funds · vuln/input-validation/boundary

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.