Reproduced Exploit
IntentX: claim() reads pending rewards for the zero address, then wipes the caller's real balance unpaid
Chain
Other
Category
untagged
Date
Jan 1970
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: 59427-user-pending-rewards-can-never-be-paid-out-quantstamp-intent. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/reward-accounting · vuln/uninitialized-read · vuln/reward-loss
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable function is reproduced verbatim (marked
@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
claim() reads _amountOut = pendingRewards[_owner] (line 89) while _owner is still the zero address (== 0), accumulates only the per-token rewards, then sets _owner to the caller (line 92) and wipes pendingRewards[_owner] = 0 (line 93). The caller's real accrued pending balance is never included in the payout total and is zeroed — the staker permanently loses their rewards.
function claim(uint256 _tokenId) external returns (uint256) {
address _owner;
uint256 _amountOut = pendingRewards[_owner]; // @> reads pendingRewards[address(0)] == 0, not the caller's balance
// add up the rewards of the xINTX token to the (wrongly-zero) pending total
Why it's exploitable here#
_amountOut = pendingRewards[_owner]executes before_owneris resolved to the caller (it readspendingRewards[address(0)] == 0).- The payout total therefore never includes the caller's accrued rewards.
pendingRewards[_owner] = 0then wipes the caller's real balance, so the rewards can never be claimed again.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in StakedINTX:
- Line 84 — _ownerOf(_tokenId) returns msg.sender — but only used AFTER the pending balance was already read for address(0).
- Line 91 — VULN. _amountOut accumulates only tokenRewards; the caller's pendingRewards (read as 0 on line 89 for address(0)) is never included.
- Line 94 — rewardToken.transfer pays _amountOut, and the caller's real pending balance was wiped to 0 (line 93) — permanently lost.
PoC#
Registry (Foundry, local deploy — exploit path + a fixed-variant control):
cd 59427-user-pending-rewards-can-never-be-paid-out-quantstamp-inte_exp
forge test -vv
Expected: both tests PASS — the exploit test asserts the caller receives 0 while 1,000 was accrued and wiped; the fixed order pays the caller their full pending balance. The browser EVM Playground is served at /hacks/59427-user-pending-rewards-can-never-be-paid-out-quantstamp-inte/.
Remediation#
Resolve _owner to the caller BEFORE reading pendingRewards, include that balance in the payout, then zero it.
References#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/59427-user-pending-rewards-can-never-be-paid-out-quantstamp-intent.md
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: 59427-user-pending-rewards-can-never-be-paid-out-quantstamp-intent.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "IntentX: claim() reads pending rewards for the zero address, then wipes the caller's real balance unpaid".
- 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.