Reproduced Exploit
Ajna H-05 — cross-pool reward-cap underflow bricks unstake / claimRewards
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: 20073-h-05-incorrect-calculation-of-the-remaining-updatedrewards-l. Standalone Foundry PoC and full write-up: 20073-h-05-incorrect-calculation-of-the-remaining-updatedrewards-l_exp in the
evm-hack-registrymirror.
Real, local (no-fork) reproduction. It deploys the real audited Ajna
ERC20PoolFactory / PositionManager / RewardsManager / two ERC20Pools
(vendored unmodified under src/ajna/), drives real reserve-auction burns in
both pools, and shows a legitimate staker's NFT getting permanently locked by an
arithmetic underflow in the audited RewardsManager.
_shared/run-poc/run_poc.sh 20073-h-05-incorrect-calculation-of-the-remaining-updatedrewards-l_exp -vvvvv
Sources: AuditVault finding #20073, Ajna repository @ commit 276942bc2f97488d07b887c8edceaaab7a5c3964.
Root cause#
RewardsManager tracks the rewards handed out per epoch across ALL pools:
mapping(uint256 => uint256) public rewardsClaimed; // epoch => rewards claimed (all pools)
mapping(uint256 => uint256) public updateRewardsClaimed; // epoch => update rewards (all pools)
but the reward cap it compares against is computed from a single pool's
burn (src/ajna/src/RewardsManager.sol:719-725):
uint256 rewardsCap = Maths.wmul(UPDATE_CAP, totalBurned); // THIS pool only
uint256 rewardsClaimedInEpoch = updateRewardsClaimed[curBurnEpoch]; // ALL pools
if (rewardsClaimedInEpoch + updatedRewards_ >= rewardsCap) {
updatedRewards_ = rewardsCap - rewardsClaimedInEpoch; // @audit underflow if global > per-pool cap
}
The same bug exists in _calculateNewRewards (RewardsManager.sol:546-549,
newRewards_ = rewardsCapped - rewardsClaimedInEpoch_). When a large pool has
pushed the shared updateRewardsClaimed[epoch] above a small pool's per-pool
rewardsCap, the subtraction underflows (Solidity 0.8 panic 0x11) and reverts.
Because this code runs inside stake / unstake / claimRewards /
updateBucketExchangeRatesAndClaim, every one of those actions reverts for the
small pool — the staked NFT (an LP position) is permanently trapped.
Exploit walkthrough (concrete numbers from the passing test)#
- A staker stakes a real LP-NFT in a big pool and a small pool (epoch 0).
- Both pools run a real reserve-auction burn -> burn epoch 1:
- big pool burns 84.12 AJNA -> per-pool cap
0.1 * 84.12 = 8.41 AJNA - small pool burns 8.45 AJNA -> per-pool cap
0.1 * 8.45 = 0.845 AJNA
- big pool burns 84.12 AJNA -> per-pool cap
updateBucketExchangeRatesAndClaim(bigPool)records the shared trackerupdateRewardsClaimed[1] = 4.21 AJNA.- The small pool staker calls
unstake->_updateBucketExchangeRatescomputesrewardsCap(0.845) - updateRewardsClaimed[1](https://github.com/sanbir/evm-hack-registry/blob/main/20073-h-05-incorrect-calculation-of-the-remaining-updatedrewards-l_exp/4.21)-> arithmetic underflow -> revert.claimRewardsreverts identically.
Harm: the small pool staker's NFT stays locked in RewardsManager
(ownerOf == RewardsManager); they can never unstake or claim. A baseline
snapshot in the test confirms the same unstake succeeds when the shared tracker
is still 0, isolating the cross-pool contamination as the cause.
Fix#
Saturate instead of subtracting: if rewardsClaimedInEpoch >= rewardsCap, set the
increment to 0 rather than computing rewardsCap - rewardsClaimedInEpoch
(apply at both RewardsManager.sol:549 and :725).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 20073-h-05-incorrect-calculation-of-the-remaining-updatedrewards-l_exp (evm-hack-registry mirror).
- AuditVault finding: 20073-h-05-incorrect-calculation-of-the-remaining-updatedrewards-l.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Ajna H-05".
- 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.