Reproduced Exploit
Ajna H-06 — a bankrupt bucket wipes a lender's already-earned staking rewards
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: 20074-h-06-the-lender-could-possibly-lose-unclaimed-rewards-in-cas. Standalone Foundry PoC and full write-up: 20074-h-06-the-lender-could-possibly-lose-unclaimed-rewards-in-cas_exp in the
evm-hack-registrymirror.
Real, local (no-fork) reproduction. It deploys the real audited Ajna
ERC20PoolFactory / PositionManager / RewardsManager / ERC20Pool
(vendored unmodified under src/ajna/), stakes a real LP position, accrues
real, claimable AJNA rewards through a real reserve-auction burn, then drives
a real liquidation + settle that bankrupts the staked bucket — and shows the
already-earned rewards collapse to zero.
_shared/run-poc/run_poc.sh 20074-h-06-the-lender-could-possibly-lose-unclaimed-rewards-in-cas_exp -vvvvv
Sources: AuditVault finding #20074, Ajna repository @ commit 276942bc2f97488d07b887c8edceaaab7a5c3964.
Root cause#
When a bucket goes bankrupt after a lender's deposit, PositionManager treats the
whole tracked position as gone — getPositionIndexesFiltered filters the bucket
out and getLP returns 0 whenever depositTime <= bankruptcyTime
(src/ajna/src/PositionManager.sol), and memorializePositions zeroes
position.lps (L192-199):
if (position.depositTime != 0) {
if (_bucketBankruptAfterDeposit(pool, index, position.depositTime)) {
position.lps = 0; // @audit wipes tracked LP without ever claiming rewards
}
}
RewardsManager.calculateRewards / _calculateAndClaimRewards iterate over
getPositionIndexesFiltered(tokenId). So the moment the bucket is marked bankrupt,
the position's rewards drop to 0 — including rewards that were already earned
and claimable while the bucket was solvent. Claiming rewards has no bankruptcy
requirement, but the position is erased before those rewards can be claimed, so
they are lost. (Sponsor acknowledged the behaviour is by-design/coupling-avoidance;
it is reported as H-06.)
Exploit walkthrough (concrete numbers from the passing test)#
- A lender mints an LP-NFT in the top bucket (index 3696) and stakes it (epoch 0).
- Deep book liquidity + two borrowers are set up; interest accrues for 400 days.
- A real reserve-auction burn advances the pool to burn epoch 1. The staked
position now has real, claimable rewards:
calculateRewards(tokenId, 1) =140.78 AJNA. - A real liquidation of the large borrower (
kick->take->settle) consumes the top bucket's deposit and marks it bankrupt. - The exact same query now returns
calculateRewards(tokenId, 1) =0, andgetLP(tokenId, 3696) = 0. Unstaking pays the staker 0 AJNA.
Harm: the lender loses 140.78 AJNA of rewards they had already earned while the bucket was solvent (measured claimable in step 3, wiped in step 4).
Fix#
Claim (or credit) a staked position's outstanding rewards before the bankrupt bucket's LP is zeroed / filtered out, so already-earned rewards are not lost when a bucket goes bankrupt.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 20074-h-06-the-lender-could-possibly-lose-unclaimed-rewards-in-cas_exp (evm-hack-registry mirror).
- AuditVault finding: 20074-h-06-the-lender-could-possibly-lose-unclaimed-rewards-in-cas.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Ajna H-06".
- 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.