Reproduced Exploit

Radiant — exit leaves earned and unlocked set and freezes 50 RDNT

exit deletes userEarnings and subtracts the paid amount from total. It does not set unlocked or earned to 0. A staker with 40 unlocked, 60 earned, and 50 locked receives 100 RDNT. total is 50, but unlocked is still 40 and earned is still 60. withdrawLock subtracts that stale withdrawable balance fr…

May 2023•Other•logic•3 min read

Chain

Other

Category

logic

Date

May 2023

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: 32846-incorrect-accounting-of-earned-and-unlocked-tokens-on-exit-m. Standalone Foundry PoC and full write-up: 32846-incorrect-accounting-of-earned-and-unlocked-tokens-on-exit-m_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/state-update · vuln/dos/frozen-funds

Key info#

ProtocolRadiant MultiFeeDistribution
FindingOpenZeppelin Radiant audit, AuditVault 32846
Vulnerable codeMultiFeeDistribution.exit
HarmExit pays 100 RDNT. The remaining 50 RDNT lock reverts and stays in the contract
Chainlocal synthetic

TL;DR#

exit deletes userEarnings and subtracts the paid amount from total. It does not set unlocked or earned to 0. A staker with 40 unlocked, 60 earned, and 50 locked receives 100 RDNT. total is 50, but unlocked is still 40 and earned is still 60. withdrawLock subtracts that stale withdrawable balance from total and underflows. The 50 RDNT stays in the contract. The fixed exit zeroes both fields, and the same lock then withdraws.

The vulnerable code#

SOLIDITY
delete userEarnings[onBehalfOf];
Balances storage bal = balances[onBehalfOf];
// @> VULN: total is reduced but unlocked and earned are left unchanged
bal.total = bal.total - amount - penaltyAmount;
_withdrawTokens(onBehalfOf, amount, penaltyAmount, burnAmount, claimRewards);

withdrawableBalance still reads balances[user].earned and balances[user].unlocked. After the array delete, the earned amount looks entirely unvested, so a later call also recomputes a penalty on tokens that were already paid.

Root cause#

Exit updates one view of the position, userEarnings and total, and leaves the two fields withdrawableBalance actually sums. The next function that subtracts a withdrawable amount from total underflows.

Preconditions#

The staker's position is 40 RDNT unlocked, 60 RDNT earned and already vested, and 50 RDNT locked. The contract holds 150 RDNT.

Attack walkthrough#

  1. exit computes a 100 RDNT withdrawable balance and a 0 penalty.
  2. It deletes the earnings array and sets total to 50.
  3. It transfers 100 RDNT.
  4. unlocked is still 40 and earned is still 60.
  5. withdrawLock reverts. The contract balance is 50 RDNT.

The control sets unlocked and earned to 0 before the transfer. withdrawLock then pays the 50 RDNT and the contract balance is 0.

Diagrams#

flowchart TD A["Position is 40 unlocked, 60 earned, 50 locked"] --> B["exit deletes earnings and pays 100 RDNT"] B --> C["total is 50 but unlocked and earned stay set"] C --> D["withdrawLock subtracts the stale balance"] D --> E["The subtraction underflows"] E --> F["50 RDNT remains in the contract"]

Impact#

The staker who calls the normal exit cannot retrieve the 50 RDNT still locked. Later withdraw and exit calls for that account revert on the total update. The fixed path returns all 150 RDNT.

Remediation#

In exit, set unlocked and earned to 0 and reduce total by those two fields before the token transfer, which is what the later Radiant commit does.

How to reproduce#

BASH
cd 32846-incorrect-accounting-of-earned-and-unlocked-tokens-on-exit-m_exp
forge test -vv

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.