Reproduced Exploit

Mystic Finance syncRewards: lastRewardAmount stores the cycle delta

Feb 2025•Other•logic•2 min read

Chain

Other

Category

logic

Date

Feb 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: 57712-c-01-miscalculation-in-rewardswill-give-toomuchrewardstouser. Standalone Foundry PoC and full write-up: 57712-c-01-miscalculation-in-rewardswill-give-toomuchrewardstouser_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/reward-calculation

Reproduction: faithful local reproduction of AuditVault 57712, using the report's own 10 ether cycle numbers. nextRewards = yieldEth - lastRewardAmount, then lastRewardAmount is set to nextRewards instead of cumulative yield.

Root cause#

lastRewardAmount is supposed to track cumulative yield. The function stores only the latest delta. The next cycle subtracts that stale delta from a larger yieldEth and books too much.

SOLIDITY
uint256 nextRewards = yieldEth - lastRewardAmount;
accountedRewards += nextRewards;
// @> VULN: lastRewardAmount stores the cycle delta, not cumulative yieldEth
lastRewardAmount = nextRewards;
accPerShare += nextRewards * 1e18 / totalShares;

Cycle 1: yield 10, next 10, last becomes 10. Cycle 2: yield 20, next 10, last is stored as 10 instead of 20. Cycle 3: yield 30, next is 20. Accounted rewards are 40 against 30 of real yield.

Attack path#

flowchart TD A["Two equal stakers join"] --> B["Cycle 1 sync books 10"] B --> C["Cycle 2 stores lastRewardAmount as 10 not 20"] C --> D["Cycle 3 books 20 on 10 of new yield"] D --> E["Books say 40 while only 30 was deposited"] E --> F["First withdrawer takes 20"] F --> G["Second staker is left with 10 and the sink records the stolen 5"]

Impact#

Fair shares of the 30 ether deposited are 15 and 15. The first withdrawer receives 20 ether. The other staker can withdraw only the remaining 10 ether, a 5 ether loss. That 5 ether is forwarded to the sink. The fixed syncRewards sets lastRewardAmount = yieldEth, accounts 30 ether, and pays each staker 15 ether.

PoC#

BASH
cd 57712-c-01-miscalculation-in-rewardswill-give-toomuchrewardstouser_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.