Reproduced Exploit
Lend V2: `_handleBorrowCrossChainRequest` distributes LEND on an already-inflated balance
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: 773. Standalone Foundry PoC and full write-up: 58384-lend-incorrect-distribution-of-lend-tokens-to-users_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic · vuln/accounting
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable
_handleBorrowCrossChainRequestordering is reproduced verbatim (marked@>) alongside the verbatimdistributeBorrowerLend/borrowWithInterestreward accounting, with faithful minimal doubles; local deploy, no fork.
Root cause#
In Lend-V2/src/LayerZero/CrossChainRouter.sol, _handleBorrowCrossChainRequest first updates the borrower's crossChainCollaterals entry — adding payload.amount to principle — and only afterwards calls distributeBorrowerLend. But distributeBorrowerLend derives the borrower's LEND reward from that very mapping (via borrowWithInterest), so distribution runs on the already-inflated balance. The vulnerable ordering, reproduced verbatim:
/**
* @dev If existing cross-chain collateral, update it. Otherwise, add new collateral.
*/
if (found) {
uint256 newPrincipleWithAmount = (userCrossChainCollaterals[index].principle * currentBorrowIndex)
/ userCrossChainCollaterals[index].borrowIndex;
userCrossChainCollaterals[index].principle = newPrincipleWithAmount + payload.amount;
userCrossChainCollaterals[index].borrowIndex = currentBorrowIndex;
lendStorage.updateCrossChainCollateral(
payload.sender, destUnderlying, index, userCrossChainCollaterals[index]
);
} else {
lendStorage.addCrossChainCollateral(
payload.sender,
destUnderlying,
LendStorage.Borrow({
srcEid: srcEid,
destEid: currentEid,
principle: payload.amount,
borrowIndex: currentBorrowIndex,
borrowedlToken: payload.destlToken,
srcToken: payload.srcToken
})
);
}
// Track borrowed asset
lendStorage.addUserBorrowedAsset(payload.sender, payload.destlToken);
// Distribute LEND rewards on destination chain
@> lendStorage.distributeBorrowerLend(payload.destlToken, payload.sender);
distributeBorrowerLend computes borrowerAmount from borrowWithInterest(...) + borrowWithInterestSame(...) and multiplies it by the elapsed LEND index delta. Because the mapping already contains the newly-borrowed payload.amount, the borrower is rewarded LEND for the entire prior index-delta period on funds it received only this instant. The fix is to distribute first, then update the mapping.
Why it's exploitable here#
Following the finding, using the reproduction's concrete values (EXISTING_PRINCIPLE = 1000e18, NEW_BORROW = 1000e18):
- A borrower already holds a cross-chain collateral position of
1000e18principle, checkpointed at the LEND index1e36; the LEND borrow index has since advanced one full period to2e36, sodeltaIndex = 1.0. - A new cross-chain borrow of
1000e18is handled. The router adds it toprinciplefirst, so the stored balance becomes2000e18before any reward is paid. distributeBorrowerLendthen reads that inflated2000e18viaborrowWithInterestand credits2000e18 * 1.0 = 2000e18LEND.- Distributing in the correct order (before the update) would credit only
1000e18 * 1.0 = 1000e18LEND. The borrower is over-credited exactly1000e18LEND — the reward on funds borrowed only this instant, for a period it never held them — draining the protocol's LEND reserve.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xaf38a9c5…:
- L407 — Enter cross-chain borrow handler: The destination-chain handler receives the LayerZero borrow message, accrues interest, and resolves the borrowed lToken's underlying token.
- L421 — Load borrower's existing collateral entries: Reads the borrower's existing
crossChainCollateralsarray for this underlying — the very mapping the LEND reward math will read later. - L430 — Find the matching collateral entry: Scans the array for the entry matching this
srcEidandsrcToken, settingfoundso the existing position is updated rather than appended. - L443 — Check collateral covers total borrow: Reads the borrower's total borrowed for the collateral-sufficiency
require, then disburses the freshly requested borrow to the user. - L454 — Update collateral before rewards: The matched entry's
principleis bumped bypayload.amount, inflating the stored balance ahead of any LEND reward payout. - L464 — Write inflated principle to mapping:
updateCrossChainCollateralpersists the new principle intocrossChainCollaterals, so the mapping now holds an amount just received this instant. - L485 — Distribute LEND on inflated balance: Root cause:
distributeBorrowerLendruns on the just-inflated mapping, soborrowWithInterestrewards the new borrow over the prior index delta — over-crediting LEND.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58384-lend-incorrect-distribution-of-lend-tokens-to-users_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: the buggy update-then-distribute order runs the verbatim distributeBorrowerLend on the inflated balance and over-credits the borrower 1000e18 LEND versus the correct distribute-then-update order. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58384-lend-incorrect-distribution-of-lend-tokens-to-users_exp (evm-hack-registry mirror).
- AuditVault finding: 773.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Lend V2:
_handleBorrowCrossChainRequestdistributes LEND on an already-inflated balance". - 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.