Reproduced Exploit
LEND: `claimLend` never resets accrued rewards, draining LEND reserves
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: 148. Standalone Foundry PoC and full write-up: 58370-lend-repeated-claims-of-the-same-rewards-drain-lend-reserves_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/theft · vuln/logic
Reproduction: a faithful minimal reproduction of the vulnerable finding — the
claimLendreward-grant loop andgrantLendInternalare reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
In Lend-V2/src/LayerZero/CoreRouter.sol's claimLend(), the final loop grants each holder their accrued LEND via grantLendInternal() but ignores its return value and never resets lendStorage.lendAccrued. Because the accrued balance survives the payout, a user can call claimLend() again and again and re-collect the same reward every time. The vulnerable lines, reproduced verbatim:
for (uint256 j = 0; j < holders.length;) {
uint256 accrued = lendStorage.lendAccrued(holders[j]);
if (accrued > 0) {
@> grantLendInternal(holders[j], accrued);
}
unchecked {
++j;
}
}
grantLendInternal() transfers the LEND and returns the un-granted remainder (0 on success), but the caller discards it. Compound's correct implementation writes that remainder back — lendAccrued[holders[j]] = grantLendInternal(holders[j], lendAccrued[holders[j]]); — zeroing the balance so a reward is paid exactly once. Here the balance is left untouched.
Why it's exploitable here#
Following the synthetic reproduction:
- The attacker legitimately accrues
100e18LEND exactly once. The router holds1000e18of LEND reserves funded for all other users. - The attacker calls
claimLend([attacker], …).grantLendInternaltransfers100e18but leaveslendStorage.lendAccrued[attacker] == 100e18. - The attacker calls
claimLendagain — the same100e18is paid out. Five repeats pay500e18more. - After 6 calls the attacker holds
600e18for a single100e18entitlement —500e18drained straight out of other users' reserves.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xbd4fd5a3…:
- L42 — Deploy the LEND reward token: Setup: the LEND reward token is deployed and minted into the router as the shared reserve that backs every user's accrued rewards.
- L115 — Iterate the claim's markets: claimLend walks the caller-supplied markets, running the accrual bookkeeping before it reaches the final reward-granting loop.
- L127 — Distribute supplier accrual: For each holder, claimLend triggers supplier LEND distribution to bring the accrued reward up to date before it is paid out.
- L133 — Accrued reward is never reset: Root cause: the grant loop pays out lendStorage.lendAccrued via grantLendInternal but never resets it, so the same reward is re-claimable every call.
- L156 — Read the router's LEND reserve: grantLendInternal reads the router's LEND balance, the shared reserve, to confirm it can still cover the unchanged accrued amount.
- L158 — Pay the same reward again: While the reserve still covers the accrued amount the router transfers it once more, so each repeated claim re-pays the same reward until reserves drain.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58370-lend-repeated-claims-of-the-same-rewards-drain-lend-reserves_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: accrue 100e18 once, call claimLend 6x to receive 600e18, draining 500e18 of other users' LEND reserves. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58370-lend-repeated-claims-of-the-same-rewards-drain-lend-reserves_exp (evm-hack-registry mirror).
- AuditVault finding: 148.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "LEND:
claimLendnever resets accrued rewards, draining LEND reserves". - 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.